/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package ucar.nc2.dods;

import opendap.dap.*;
import opendap.dap.Attribute;
import opendap.dap.parsers.ParseException;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.StringTokenizer;
import java.util.List;
import java.io.IOException;
import java.io.PrintStream;

import ucar.ma2.Array;
import ucar.ma2.DataType;
import ucar.nc2.*;
import ucar.nc2.constants.CDM;

/**
 * DodsV: a container for dods basetypes, so we can track stuff as we process it.
 * We make the DodsV tree follow the name heirarchy, by putting the darray object to the side.
 * The root is just a container, but the other nodes each represent a BaseType/Variable.
 *

 * DDS has a vector of BaseTypes, which may be:
 * scalar primitive (byte, int, float, String, etc)
 * array of primitive: DArray with (empty) PrimitiveVector with "template" as the BaseType.
 * DConstructor is a container for other BaseTypes, can call getVariables() to get them
 * - DStructure
 * - DGrid first Variable is the Array, rest are the maps
 * - DSequence
 * array of DStructure: DArray with BaseTypePrimitiveVector whose template is a DStructure
 *

 * DataDDS also contains the data:
 * scalar primitive (byte, int, float, String, etc): getValue()
 * array of primitive: DArray with PrimitiveVector (BytePrimitiveVector, FloatPrimitiveVector, etc): getValue(i)
 * Get internal java array with getInternalStorage().
 * array of String: DArray with BaseTypePrimitiveVector containing String objects
 * scalar DStructure, DGrid: same as DDS
 * array of DStructure: DArray with BaseTypePrimitiveVector, whose values are DStructure
 * array of DGrid, DSequence: (not sure how to interpret)
 * DSequence: values = Vector (rows) containing Vector (fields)
 *
 * @author caron
 */

class DodsV implements Comparable {
  static private org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(DodsV.class);
  static private boolean debugAttributes = false;

  /**
   * Parse the DDS, creating a tree of DodsV objects. The root node is only a container, ie it has no BaseType.
   * The Darray object (which has the dimension info) becomes a field of the DodsV, rather than being in the tree.
   *
   * @param dds the DDS to parse
   * @return the root dodsV object
   */
  static DodsV parseDDS(DDS dds) {
    DodsV root = new DodsV(null, null);

    // recursively get the Variables from the DDS
    Enumeration variables = dds.getVariables();
    parseVariables(root, variables);

    // assign depth first sequence number
    root.assignSequence(root);
    return root;
  }

  /**
   * Recursively build the dodsV tree.
   * 1) put all Variables into a DodsV
   * 2) unravel DConstructors (DSequence, DStructure, DGrid)
   * 3) for Darray, we put Variable = elemType, and store the darray seperately, not in the heirarchy.
   *
   * @param parent   of the tree
   * @param children list of BaseType
   */
  static private void parseVariables(DodsV parent, Enumeration children) {
    while (children.hasMoreElements()) {
      opendap.dap.BaseType bt = (opendap.dap.BaseType) children.nextElement();

      if (bt instanceof DList) {
        String mess = "Variables of type " + bt.getClass().getName() + " are not supported.";
        logger.warn(mess);
        continue;
      }

      DodsV dodsV = new DodsV(parent, bt);

      if (bt instanceof DConstructor) {
        DConstructor dcon = (DConstructor) bt;
        java.util.Enumeration enumerate2 = dcon.getVariables();
        parseVariables(dodsV, enumerate2);

      } else if (bt instanceof DArray) {
        DArray da = (DArray) bt;

        // Check to see if the array has any zero-length dimension; if so then ignore
        for (Enumeration e = da.getDimensions(); e.hasMoreElements(); ) {
          DArrayDimension dim = (DArrayDimension) e.nextElement();
          if (dim.getSize() <= 0) return;
        }

        BaseType elemType = da.getPrimitiveVector().getTemplate();
        dodsV.bt = elemType;
        dodsV.darray = da;

        if ((elemType instanceof DGrid) || (elemType instanceof DSequence) || (elemType instanceof DList)) {
          String mess = "Arrays of type " + elemType.getClass().getName() + " are not supported.";
          logger.warn(mess);
          continue;
        }

        if (elemType instanceof DStructure) { // note that for DataDDS, cant traverse this further to find the data.
          DConstructor dcon = (DConstructor) elemType;
          java.util.Enumeration nestedVariables = dcon.getVariables();
          parseVariables(dodsV, nestedVariables);
        }
      }

      parent.children.add(dodsV);
    }
  }

  /**
   * Parse the DDS, creating a tree of DodsV objects. The root node is only a container, ie it has no BaseType.
   * The Darray object (which has the dimension info) becomes a field of the DodsV, rather than being in the tree.
   *
   * @param dds the DataDDS to parse
   * @return the root DodsV object
   * @throws opendap.dap.NoSuchVariableException if DataDDS contains a Variable not in the DDS
   */
  static DodsV parseDataDDS(DataDDS dds) throws NoSuchVariableException {
    DodsV root = new DodsV(null, null);

    // recursively get the Variables from the DDS
    Enumeration variables = dds.getVariables();
    parseDataVariables(root, variables);

    // assign depth first sequence number
    root.assignSequence(root);
    return root;
  }

  /**
   * Recursively build the dodsV tree.
   * 1) put all Variables into a DodsV
   * 2) unravel DConstructors (DSequence, DStructure, DGrid)
   * 3) for Darray, we put Variable = elemType, and store the darray seperately, not in the heirarchy.
   *
   * @param parent   of the tree
   * @param children list of BaseType
   * @throws opendap.dap.NoSuchVariableException if children and parent are inconsistent
   */
  static private void parseDataVariables(DodsV parent, Enumeration children) throws NoSuchVariableException {
    while (children.hasMoreElements()) {
      opendap.dap.BaseType bt = (opendap.dap.BaseType) children.nextElement();
      DodsV dodsV = new DodsV(parent, bt);
      parent.children.add(dodsV);

      if (bt instanceof DGrid) {
        DGrid dgrid = (DGrid) bt;
        if (dodsV.parent.bt == null) { // is top level
          // top level grids are replaced by their "data array"
          dodsV.darray = (DArray) dgrid.getVar(0);
          processDArray(dodsV);
        } else {
          // nested grids are made into Structures
          dodsV.makeAllDimensions();
        }

        java.util.Enumeration enumerate2 = dgrid.getVariables();
        parseDataVariables(dodsV, enumerate2);

      } else if (bt instanceof DSequence) {
        DSequence dseq = (DSequence) bt;
        int seqlen = dseq.getRowCount();
        if (seqlen > 0) {
          DArrayDimension ddim = new DArrayDimension(seqlen, null);
          dodsV.dimensions.add(ddim);
        }
        dodsV.makeAllDimensions();

        java.util.Enumeration enumerate2 = dseq.getVariables();
        parseDataVariables(dodsV, enumerate2);

      } else if (bt instanceof DConstructor) {
        DStructure dcon = (DStructure) bt; // LOOK DConstructor != DStructure?
        dodsV.makeAllDimensions();
        java.util.Enumeration enumerate2 = dcon.getVariables();
        parseDataVariables(dodsV, enumerate2);

      } else if (bt instanceof DArray) {
        dodsV.darray = (DArray) bt;
        processDArray(dodsV);

        dodsV.bt = dodsV.elemType;
        if (dodsV.elemType instanceof DStructure) {
          DStructure dcon = (DStructure) dodsV.elemType;
          java.util.Enumeration nestedVariables = dcon.getVariables();
          parseDataVariables(dodsV, nestedVariables);
        }
      } else {
        dodsV.makeAllDimensions();
      }

    }
  }

  static private void processDArray(DodsV dodsV) {
    DArray da = dodsV.darray;
    Enumeration dims = da.getDimensions();
    while (dims.hasMoreElements()) {
      DArrayDimension dim = (DArrayDimension) dims.nextElement();
      dodsV.dimensions.add(dim);
    }
    dodsV.makeAllDimensions(); // redo

    BaseType elemType;
    PrimitiveVector pv = da.getPrimitiveVector();
    if (pv instanceof BaseTypePrimitiveVector) {
      BaseTypePrimitiveVector bpv = (BaseTypePrimitiveVector) pv;
      elemType = bpv.getValue(0);
    } else {
      elemType = da.getPrimitiveVector().getTemplate();
    }

    dodsV.elemType = elemType;

    if ((dodsV.elemType instanceof DGrid) || (dodsV.elemType instanceof DSequence) || (dodsV.elemType instanceof DList)) {
      String mess = "Arrays of type " + dodsV.bt.getClass().getName() + " are not supported.";
      logger.error(mess);
      throw new IllegalArgumentException(mess);
    }

  }

  //////////////////////////////////////////////////////////////////

  DodsV parent;
  BaseType bt;
  BaseType elemType; // different for DGrid
  List<DodsV> children = new ArrayList<>(); // DodsV objects
  DArray darray; // if its an array
  List<DArrayDimension> dimensions = new ArrayList<>();
  List<DArrayDimension> dimensionsAll = new ArrayList<>();
  List<DODSAttribute> attributes = new ArrayList<>();

  Array data; // preload
  boolean isDone; // nc var has been made
  int seq; // "depth first" order

  DodsV(DodsV parent, BaseType bt) {
    this.parent = parent;
    this.bt = bt;
    this.elemType = bt;
  }

  public int compareTo(Object o) {
    if (o instanceof DodsV)
      return seq - ((DodsV) o).seq;
    return -1;
  }

  void show(PrintStream out, String space) {
    out.print(space + "DodsV.show " + getEncodedName() + " " + getType());
    out.print("(");
    int count = 0;
    for (DArrayDimension dim : dimensionsAll) {
      String name = dim.getEncodedName() == null ? "" : dim.getEncodedName() + "=";
      if (count > 0) out.print(",");
      out.print(name + dim.getSize());
      count++;
    }
    out.println(")");

    for (DodsV dodsV : children) {
      dodsV.show(out, space + "  ");
    }
  }

  @Override
  public String toString() {
    return bt.getLongName();
  }

  //String getName() { return bt == null ? " root" : bt.getEncodedName(); }

  String getClearName() {
    return bt == null ? "root" : bt.getClearName();
  }

  String getEncodedName() {
    return bt == null ? "root" : bt.getEncodedName();
  }

  String getFullName() {
    if (parent != null && parent.bt != null)
      return (parent.getFullName() + "." + bt.getEncodedName());
    return (bt == null) ? "root" : bt.getEncodedName();
  }

  String getNetcdfShortName() {
    return DODSNetcdfFile.makeShortName(getClearName());
  }

  String getType() {
    return bt == null ? "" : bt.getTypeName();
  }

  DataType getDataType() {
    if (bt == null)
      throw new IllegalStateException("DodsV.bt  == null");
    if (bt instanceof DGrid)
      return DODSNetcdfFile.convertToNCType(elemType, isUnsigned());
    else
      return DODSNetcdfFile.convertToNCType(bt, isUnsigned());
  }

  public boolean isUnsigned() {
    ucar.nc2.Attribute att = findAttributeIgnoreCase(CDM.UNSIGNED);
    return (att == null) || att.getStringValue().equalsIgnoreCase("true");
  }

  public ucar.nc2.Attribute findAttributeIgnoreCase(String name) {
    for (ucar.nc2.Attribute a : attributes) {
      if (name.equalsIgnoreCase(a.getShortName()))
        return a;
    }
    return null;
  }

  int[] getShape() {
    int[] shape = new int[dimensions.size()];
    for (int i = 0; i < dimensions.size(); i++) {
      DArrayDimension dim = dimensions.get(i);
      shape[i] = dim.getSize();
    }
    return shape;
  }

  int[] getShapeAll() {
    if (bt instanceof DSequence) {
      DSequence dseq = (DSequence) bt;
      int seqlen = dseq.getRowCount();
      return new int[]{seqlen};
    }

    int[] shape = new int[dimensionsAll.size()];
    for (int i = 0; i < dimensionsAll.size(); i++) {
      DArrayDimension dim = dimensionsAll.get(i);
      shape[i] = dim.getSize();
    }
    return shape;
  }

  void addAttribute(DODSAttribute att) {
    attributes.add(att);
  }

  void makeAllDimensions() {
    dimensionsAll = new ArrayList<>();
    if (parent != null)
      dimensionsAll.addAll(parent.dimensionsAll);
    dimensionsAll.addAll(dimensions);
  }

  // assign depth first sequence number
  private int nextInSequence = 0;

  private void assignSequence(DodsV root) {
    for (DodsV nested : children) {
      nested.assignSequence(root);
      nested.seq = root.nextInSequence;
      nextInSequence++;
    }
  }

  /**
   * Parse the DAS, assign attribute tables to the DodsV objects.
   * Nested attribute tables are supposed to follow the tree we construct with dodsV, so its easy to assign to correct dodsV.
   *
   * @param das parse this DAS
   * @throws IOException on io error
   */
  void parseDAS(DAS das) throws IOException {
    Enumeration tableNames = das.getNames();
    while (tableNames.hasMoreElements()) {
      String tableName = (String) tableNames.nextElement();
      AttributeTable attTable = das.getAttributeTableN(tableName);

      if (tableName.equals("NC_GLOBAL") || tableName.equals("HDF_GLOBAL")) {
        addAttributeTable(this, attTable, tableName, true);

      } else if (tableName.equals("DODS_EXTRA") || tableName.equals("EXTRA_DIMENSION")) {
        // handled seperately in DODSNetcdfFile
        continue;

      } else {
        DodsV dodsV = findDodsV(tableName, false); // short name matches the table name
        if (dodsV != null) {
          addAttributeTable(dodsV, attTable, tableName, true);
        } else {
          dodsV = findTableDotDelimited(tableName);
          if (dodsV != null) {
            addAttributeTable(dodsV, attTable, tableName, true);
          } else {
            if (debugAttributes) System.out.println("DODSNetcdf getAttributes CANT find <" + tableName + "> add to globals");
            addAttributeTable(this, attTable, tableName, false);
          }
        }
      }
    }
  }

  private void addAttributeTable(DodsV dodsV, AttributeTable attTable, String fullName, boolean match) {
    if (attTable == null) return;

    java.util.Enumeration attNames = attTable.getNames();
    while (attNames.hasMoreElements()) {
      String attName = (String) attNames.nextElement();
      opendap.dap.Attribute att = attTable.getAttribute(attName);
      if (att == null) {
        logger.error("Attribute not found=" + attName + " in table=" + attTable.getEncodedName());
        continue;
      }
      addAttribute(dodsV, att, fullName, match);
    }
  }

  private void addAttribute(DodsV dodsV, opendap.dap.Attribute att, String fullName, boolean match) {
    if (att == null) return;
    fullName = fullName + "." + att.getEncodedName();

    if (!att.isContainer()) {
      DODSAttribute ncatt = new DODSAttribute(match ? att.getEncodedName() : fullName, att);
      dodsV.addAttribute(ncatt);
      if (debugAttributes) System.out.println(" addAttribute " + ncatt.getShortName() + " to " + dodsV.getFullName());

    } else if (att.getEncodedName() == null) {
      logger.info("DODS attribute name is null = " + att);
    } else {
      DodsV child = dodsV.findDodsV(att.getEncodedName(), false);
      if (child != null) {
        addAttributeTable(child, att.getContainerN(), fullName, match);
      } else {
        if (att.getEncodedName().equals("DODS")) return; // special case - DODS info
        if (debugAttributes) System.out.println(" Cant find nested Variable " + att.getEncodedName() + " in " + dodsV.getFullName());
        addAttributeTable(this, att.getContainerN(), fullName, false);
      }
    }
  }

  /**
   * Search the immediate children for a BaseType with given name.
   *
   * @param name    look for this name
   * @param useDone
   * @return child that matches if found, else null
   */
  DodsV findDodsV(String name, boolean useDone) {
    for (DodsV dodsV : children) {
      if (useDone && dodsV.isDone) continue; // LOOK useDone ??
      if ((name == null) || (dodsV == null) || (dodsV.bt == null)) {
        logger.warn("Corrupted structure");
        continue;
      }
      if (name.equals(dodsV.bt.getEncodedName()))
        return dodsV;
    }
    return null;
  }

  DodsV findByNetcdfShortName(String ncname) {
    for (DodsV child : children) {
      if (ncname.equals(child.getNetcdfShortName()))
        return child;
    }
    return null;
  }

  DodsV findByDodsShortName(String dodsname) {
    for (DodsV child : children) {
      if (dodsname.equals(child.getClearName()))
        return child;
    }

    for (DodsV child : children) {
      DodsV d = child.findByDodsShortName(dodsname);
      if (null != d) return d;
    }

    return null;
  }

  // From a ddsV tree, we need to find the corresponding dodsV in the DataDDS tree.
  // find the DodsV object in the dataVlist corresponding to the ddsV
  DodsV findDataV(DodsV ddsV) {
    if (ddsV.parent.bt != null) {
      DodsV parentV = findDataV(ddsV.parent);
      if (parentV == null) // dataDDS may not have the structure wrapper
        return findDodsV(ddsV.bt.getEncodedName(), true);
      return parentV.findDodsV(ddsV.bt.getEncodedName(), true);
    }

    DodsV dataV = findDodsV(ddsV.bt.getEncodedName(), true);
    /* if ((dataV == null) && (ddsV.bt instanceof DGrid)) { // when asking for the Grid array
      DodsV gridArray = (DodsV) ddsV.children.get(0);
      return findDodsV( gridArray.bt.getName(), dataVlist, true);
    } */
    return dataV;
  }

  DodsV findTableDotDelimited(String tableName) {
    DodsV dodsV = this;
    StringTokenizer toker = new StringTokenizer(tableName, ".");
    while (toker.hasMoreTokens()) {
      String name = toker.nextToken();
      dodsV = dodsV.findDodsV(name, false);
      if (dodsV == null) return null;
    }
    return dodsV;
  }

  // Additions to help support RC.getUseGroups

  /**
   * Return number of children
   *
   * @return |children|
   */
  int getChildCount() {
    return children.size();
  }

  /**
   * Return a child by index
   *
   * @param index return index'th child
   * @return index'th child or null if index is too large
   */
  DodsV findByIndex(int index) {
    if (children.size() <= index) return null;
    return children.get(index);
  }

  public BaseType getBaseType() {
    return bt;
  }

  ////////////////////////////////////////////////////////////////////////////////
  private static void doit(String urlName) throws IOException, DAP2Exception {
    System.out.println("DODSV read =" + urlName);
    try (DConnect2 dodsConnection = new DConnect2(urlName, true)) {

      // get the DDS
      DDS dds = dodsConnection.getDDS();
      dds.print(System.out);
      DodsV root = DodsV.parseDDS(dds);

      // get the DAS
      DAS das = dodsConnection.getDAS();
      das.print(System.out);
      root.parseDAS(das);

      // show the dodsV tree
      root.show(System.out, "");
    }
  }

  public static void main(String args[]) throws IOException, DAP2Exception {
    // doit("http://localhost:8080/thredds/dodsC/ncdodsTest/conventions/zebra/SPOL_3Volumes.nc");
    doit("http://iridl.ldeo.columbia.edu/SOURCES/.CAYAN/dods");
  }

}


/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package ucar.nc2;

import ucar.ma2.*;
import ucar.nc2.constants.CDM;
import ucar.nc2.constants.CF;
import ucar.nc2.iosp.IospHelper;
import ucar.nc2.util.CancelTask;
import ucar.nc2.util.Indent;
import ucar.nc2.util.rc.RC;

import java.io.OutputStream;
import java.util.*;
import java.io.IOException;
import java.nio.channels.WritableByteChannel;

/**
 * A Variable is a logical container for data. It has a dataType, a set of Dimensions that define its array shape,
 * and optionally a set of Attributes.
 *

 * The data is a multidimensional array of primitive types, Strings, or Structures.
 * Data access is done through the read() methods, which return a memory resident Array.
 *
 Immutable if setImmutable() was called.
 *
 * @author caron
 * @see ucar.ma2.Array
 * @see ucar.ma2.DataType
 */
public class Variable extends CDMNode implements VariableIF, ProxyReader, AttributeContainer {
  /**
   * Globally permit or prohibit caching. For use during testing and debugging.
   *

   * A {@code true} value for this field does not indicate whether a Variable
   * {@link #isCaching() is caching}, only that it's permitted to cache.
   */
  static public boolean permitCaching = true;

  static public int defaultSizeToCache = 4000; // bytes  cache any variable whose size() < defaultSizeToCache
  static public int defaultCoordsSizeToCache = 40 * 1000; // bytes cache coordinate variable whose size() < defaultSizeToCache

  static protected boolean debugCaching = false;
  static private org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Variable.class);

  static public String getDAPName(String name, Variable context) {
    if (RC.getUseGroups()) {
      // leave off leading '/' for root entries
      Group xg = context.getParentGroup();
      if (!xg.isRoot()) {
        // Get the list of parent groups
        List<Group> path = Group.collectPath(xg);
        Formatter dapname = new Formatter();
        for (int i = 1; i < path.size(); i++) {   // start at 1 to skip root group
          Group g = path.get(i);
          dapname.format("/%s", g.getShortName());
        }
        dapname.format("/%s", name);
        name = dapname.toString();
      }
    }
    return name;
  }

  static public String getDAPName(Variable v) {
    return Variable.getDAPName(v.getShortName(), v);
  }

  //////////////////////////////////////////////////
  // Instance data and methods

  protected NetcdfFile ncfile; // physical container for this Variable; where the I/O happens. may be null if Variable is self contained.
  protected int[] shape = new int[0];
  protected Section shapeAsSection;  // derived from the shape, immutable; used for every read, deferred creation

  protected DataType dataType;
  protected int elementSize;
  protected List<Dimension> dimensions = new ArrayList<>(5);
  protected AttributeContainerHelper attributes;

  protected boolean isVariableLength = false;
  protected boolean isMetadata = false;

  protected Cache cache = new Cache();           // cache cannot be null
  protected int sizeToCache = -1; // bytes

  protected ProxyReader proxyReader = this;

  /**
   * Get the data type of the Variable.
   */
  public DataType getDataType() {
    return dataType;
  }

  /**
   * Get the shape: length of Variable in each dimension.
   * A scalar (rank 0) will have an int[0] shape.
   *
   * @return int array whose length is the rank of this Variable
   *         and whose values equal the length of that Dimension.
   */
  public int[] getShape() {
    int[] result = new int[shape.length];  // optimization over clone()
    System.arraycopy(shape, 0, result, 0, shape.length);
    return result;
  }

  // if scalar, return int[1], else return getShape()
  public int[] getShapeNotScalar() {
    if (isScalar())
      return new int[]{1};
    return getShape();
  }

  /**
   * Get the size of the ith dimension
   *
   * @param index which dimension
   * @return size of the ith dimension
   */
  public int getShape(int index) {
    return shape[index];
  }

  /**
   * Get the total number of elements in the Variable.
   * If this is an unlimited Variable, will use the current number of elements.
   * If this is a Sequence, will return 1.
   * If variable length, will skip vlen dimensions
   *
   * @return total number of elements in the Variable.
   */
  public long getSize() {
    long size = 1;
    for (int aShape : shape) {
      if (aShape >= 0)
        size *= aShape;
    }
    return size;
  }

  /**
   * Get the number of bytes for one element of this Variable.
   * For Variables of primitive type, this is equal to getDataType().getSize().
   * Variables of String type dont know their size, so what they return is undefined.
   * Variables of Structure type return the total number of bytes for all the members of
   * one Structure, plus possibly some extra padding, depending on the underlying format.
   * Variables of Sequence type return the number of bytes of one element.
   *
   * @return total number of bytes for the Variable
   */
  public int getElementSize() {
    return elementSize;
  }

  /**
   * Get the number of dimensions of the Variable.
   *
   * @return the rank
   */
  public int getRank() {
    return shape.length;
  }

  /**
   * Get the parent group.
   *
   * @return group of this variable; if null return rootgroup
   */
  public Group getParentGroup() {
    Group g = super.getParentGroup();
    if (g == null) {
      g = ncfile.getRootGroup();
      super.setParentGroup(g);
    }

    assert g != null;
    return g;
  }

  /**
   * Is this variable metadata?. True if its values need to be included explicitly in NcML output.
   *
   * @return true if Variable values need to be included in NcML
   */
  public boolean isMetadata() {
    return isMetadata;
  }

  /**
   * Whether this is a scalar Variable (rank == 0).
   *
   * @return true if Variable has rank 0
   */
  public boolean isScalar() {
    return getRank() == 0;
  }

  /**
   * Does this variable have a variable length dimension?
   * If so, it has as one of its dimensions Dimension.VLEN.
   *
   * @return true if Variable has a variable length dimension?
   */
  public boolean isVariableLength() {
    return isVariableLength;
  }

  /**
   * Can this variable's size grow?.
   * This is equivalent to saying at least one of its dimensions is unlimited.
   *
   * @return boolean true iff this variable can grow
   */
  public boolean isUnlimited() {
    for (Dimension d : dimensions) {
      if (d.isUnlimited()) return true;
    }
    return false;
  }

  /**
   * Get the list of dimensions used by this variable.
   * The most slowly varying (leftmost for Java and C programmers) dimension is first.
   * For scalar variables, the list is empty.
   *
   * @return List<Dimension>, immutable
   */
  public java.util.List<Dimension> getDimensions() {
    return dimensions;
  }

  /**
   * Get the ith dimension.
   *
   * @param i index of the dimension.
   * @return requested Dimension, or null if i is out of bounds.
   */
  public Dimension getDimension(int i) {
    if ((i < 0) || (i >= getRank())) return null;
    return dimensions.get(i);
  }

  /**
   * Get the list of Dimension names, space delineated.
   *
   * @return Dimension names, space delineated
   */
  public String getDimensionsString() {
    return Dimension.makeDimensionsString(dimensions);
  }

  /**
   * Find the index of the named Dimension in this Variable.
   *
   * @param name the name of the dimension
   * @return the index of the named Dimension, or -1 if not found.
   */
  public int findDimensionIndex(String name) {
    for (int i = 0; i < dimensions.size(); i++) {
      Dimension d = dimensions.get(i);
      if (name.equals(d.getShortName()))
        return i;
    }
    return -1;
  }

  /**
   * Get the description of the Variable.
   * Default is to use CDM.LONG_NAME attribute value. If not exist, look for "description", "title", or
   * "standard_name" attribute value (in that order).
   *
   * @return description, or null if not found.
   */
  public String getDescription() {
    String desc = null;
    Attribute att = findAttributeIgnoreCase(CDM.LONG_NAME);
    if ((att != null) && att.isString())
      desc = att.getStringValue();

    if (desc == null) {
      att = findAttributeIgnoreCase("description");
      if ((att != null) && att.isString())
        desc = att.getStringValue();
    }

    if (desc == null) {
      att = findAttributeIgnoreCase(CDM.TITLE);
      if ((att != null) && att.isString())
        desc = att.getStringValue();
    }

    if (desc == null) {
      att = findAttributeIgnoreCase(CF.STANDARD_NAME);
      if ((att != null) && att.isString())
        desc = att.getStringValue();
    }

    return desc;
  }

  /**
   * Get the Unit String for the Variable.
   * Looks for the CDM.UNITS attribute value
   *
   * @return unit string, or null if not found.
   */
  public String getUnitsString() {
    String units = null;
    Attribute att = findAttribute(CDM.UNITS);
    if (att == null) att = findAttributeIgnoreCase(CDM.UNITS);
    if ((att != null) && att.isString()) {
      units = att.getStringValue();
      if (units != null) units = units.trim();
    }
    return units;
  }

  /**
   * Get shape as an List of Range objects.
   * The List is immutable.
   *
   * @return List of Ranges, one for each Dimension.
   */
  public List<Range> getRanges() {
    return getShapeAsSection().getRanges();
  }

  /**
   * Get shape as a Section object.
   *
   * @return Section containing List<Range>, one for each Dimension.
   */
  public Section getShapeAsSection() {
    if (shapeAsSection == null) {
      try {
        List<Range> list = new ArrayList<>();
        for (Dimension d : dimensions) {
          int len = d.getLength();
          if (len > 0)
            list.add(new Range(d.getShortName(), 0, len - 1));
          else if (len == 0)
            list.add( Range.EMPTY); // LOOK empty not named
          else {
            assert d.isVariableLength();
            list.add( Range.VLEN); // LOOK vlen not named
          }
        }
        shapeAsSection = new Section(list).makeImmutable();

      } catch (InvalidRangeException e) {
        log.error("Bad shape in variable " + getFullName(), e);
        throw new IllegalStateException(e.getMessage());
      }
    }
    return shapeAsSection;
  }

  public ProxyReader getProxyReader() {
    return proxyReader;
  }

  public void setProxyReader(ProxyReader proxyReader) {
    this.proxyReader = proxyReader;
  }

  /**
   * Create a new Variable that is a logical subsection of this Variable.
   * No data is read until a read method is called on it.
   *
   * @param ranges List of type ucar.ma2.Range, with size equal to getRank().
   *               Each Range corresponds to a Dimension, and specifies the section of data to read in that Dimension.
   *               A Range object may be null, which means use the entire dimension.
   * @return a new Variable which is a logical section of this Variable.
   * @throws InvalidRangeException
   */
  public Variable section(List<Range> ranges) throws InvalidRangeException {
    return section(new Section(ranges, shape).makeImmutable());
  }

  /**
   * Create a new Variable that is a logical subsection of this Variable.
   * No data is read until a read method is called on it.
   *
   * @param subsection Section of this variable.
   *                   Each Range in the section corresponds to a Dimension, and specifies the section of data to read in that Dimension.
   *                   A Range object may be null, which means use the entire dimension.
   * @return a new Variable which is a logical section of this Variable.
   * @throws InvalidRangeException if section not compatible with shape
   */
  public Variable section(Section subsection) throws InvalidRangeException {

    subsection = Section.fill(subsection, shape);

    // create a copy of this variable with a proxy reader
    Variable sectionV = copy(); // subclasses must override
    sectionV.setProxyReader(new SectionReader(this, subsection));
    sectionV.shape = subsection.getShape();
    sectionV.createNewCache(); // dont share the cache
    sectionV.setCaching(false); // dont cache

    // replace dimensions if needed !! LOOK not shared
    sectionV.dimensions = new ArrayList<>();
    for (int i = 0; i < getRank(); i++) {
      Dimension oldD = getDimension(i);
      Dimension newD = (oldD.getLength() == sectionV.shape[i]) ? oldD : new Dimension(oldD.getShortName(), sectionV.shape[i], false);
      newD.setUnlimited(oldD.isUnlimited());
      sectionV.dimensions.add(newD);
    }
    sectionV.resetShape();
    return sectionV;
  }

  /**
   * Create a new Variable that is a logical slice of this Variable, by
   * fixing the specified dimension at the specified index value. This reduces rank by 1.
   * No data is read until a read method is called on it.
   *
   * @param dim   which dimension to fix
   * @param value at what index value
   * @return a new Variable which is a logical slice of this Variable.
   * @throws InvalidRangeException if dimension or value is illegal
   */
  public Variable slice(int dim, int value) throws InvalidRangeException {
    if ((dim < 0) || (dim >= shape.length))
      throw new InvalidRangeException("Slice dim invalid= " + dim);

    // ok to make slice of record dimension with length 0
    boolean recordSliceOk = false;
    if ((dim == 0) && (value == 0)) {
      Dimension d = getDimension(0);
      recordSliceOk = d.isUnlimited();
    }

    // otherwise check slice in range
    if (!recordSliceOk) {
      if ((value < 0) || (value >= shape[dim]))
        throw new InvalidRangeException("Slice value invalid= " + value + " for dimension " + dim);
    }

    // create a copy of this variable with a proxy reader
    Variable sliceV = copy(); // subclasses must override
    Section slice = new Section(getShapeAsSection());
    slice.replaceRange(dim, new Range(value, value)).makeImmutable();
    sliceV.setProxyReader(new SliceReader(this, dim, slice));
    sliceV.createNewCache(); // dont share the cache
    sliceV.setCaching(false); // dont cache

    // remove that dimension - reduce rank
    sliceV.dimensions.remove(dim);
    sliceV.resetShape();
    return sliceV;
  }

  /**
   * Create a new Variable that is a logical view of this Variable, by
   * eliminating the specified dimension(s) of length 1.
   * No data is read until a read method is called on it.
   *
   * @param dims   list of  dimensions of length 1 to reduce
   * @return a new Variable which is a logical slice of this Variable.
   * @throws InvalidRangeException if dimension or value is illegal
   */
  public Variable reduce(List<Dimension> dims) throws InvalidRangeException {
    List<Integer> dimIdx = new ArrayList<>(dims.size());
    for (Dimension d : dims) {
      assert dimensions.contains(d);
      assert d.getLength() == 1;
      dimIdx.add(dimensions.indexOf(d));
    }

    // create a copy of this variable with a proxy reader
    Variable sliceV = copy(); // subclasses must override
    sliceV.setProxyReader(new ReduceReader(this, dimIdx));
    sliceV.createNewCache(); // dont share the cache
    sliceV.setCaching(false); // dont cache

    // remove dimension(s) - reduce rank
    for (Dimension d : dims) sliceV.dimensions.remove(d);
    sliceV.resetShape();
    return sliceV;
  }

  protected Variable copy() {
    return new Variable(this);
  }

  public NetcdfFile getNetcdfFile() {
    return ncfile;
  }

  //////////////////////////////////////////////////////////////////////////////

  /**
   * Lookup the enum string for this value.
   * Can only be called on enum types, where dataType.isEnum() is true.
   *
   * @param val the integer value of this enum
   * @return the String value
   */
  public String lookupEnumString(int val) {
    if (!dataType.isEnum())
      throw new UnsupportedOperationException("Can only call Variable.lookupEnumVal() on enum types");
    return enumTypedef.lookupEnumString(val);
  }

  private EnumTypedef enumTypedef;

  /**
   * Public by accident.
   *
   * @param enumTypedef set the EnumTypedef, only use if getDataType.isEnum()
   */
  public void setEnumTypedef(EnumTypedef enumTypedef) {
    if (immutable) throw new IllegalStateException("Cant modify");
    if (!dataType.isEnum())
      throw new UnsupportedOperationException("Can only call Variable.setEnumTypedef() on enum types");
    this.enumTypedef = enumTypedef;
  }

  /**
   * Get the EnumTypedef, only use if getDataType.isEnum()
   *
   * @return enumTypedef or null if !getDataType.isEnum()
   */
  public EnumTypedef getEnumTypedef() {
    return enumTypedef;
  }

  //////////////////////////////////////////////////////////////////////////////
  // IO
  // implementation notes to subclassers
  // all other calls use them, so override only these:
  //   _read()
  //   _read(Section section)
  //   _readNestedData(Section section, boolean flatten)

  /**
   * Read a section of the data for this Variable and return a memory resident Array.
   * The Array has the same element type as the Variable, and the requested shape.
   * Note that this does not do rank reduction, so the returned Array has the same rank
   * as the Variable. Use Array.reduce() for rank reduction.
   *

   * assert(origin[ii] + shape[ii]*stride[ii] <= Variable.shape[ii]);
   *

   *
   * @param origin int array specifying the starting index. If null, assume all zeroes.
   * @param shape  int array specifying the extents in each dimension.
   *               This becomes the shape of the returned Array.
   * @return the requested data in a memory-resident Array
   */
  public Array read(int[] origin, int[] shape) throws IOException, InvalidRangeException {
    if ((origin == null) && (shape == null))
      return read();

    if (origin == null)
      return read(new Section(shape));

    if (shape == null)
      return read(new Section(origin, this.shape));

    return read(new Section(origin, shape));
  }

  /**
   * Read data section specified by a "section selector", and return a memory resident Array. Uses
   * Fortran 90 array section syntax.
   *
   * @param sectionSpec specification string, eg "1:2,10,:,1:100:10". May optionally have ().
   * @return the requested data in a memory-resident Array
   * @see ucar.ma2.Section for sectionSpec syntax
   */
  public Array read(String sectionSpec) throws IOException, InvalidRangeException {
    return read(new Section(sectionSpec));
  }

  /**
   * Read a section of the data for this Variable from the netcdf file and return a memory resident Array.
   *
   * @param ranges list of Range specifying the section of data to read.
   * @return the requested data in a memory-resident Array
   * @throws IOException           if error
   * @throws InvalidRangeException if ranges is invalid
   * @see #read(Section)
   */
  public Array read(List<Range> ranges) throws IOException, InvalidRangeException {
    if (null == ranges)
      return _read();

    return read(new Section(ranges));
  }

  /**
   * Read a section of the data for this Variable from the netcdf file and return a memory resident Array.
   * The Array has the same element type as the Variable, and the requested shape.
   * Note that this does not do rank reduction, so the returned Array has the same rank
   * as the Variable. Use Array.reduce() for rank reduction.
   *

   * If the Variable is a member of an array of Structures, this returns only the variable's data
   * in the first Structure, so that the Array shape is the same as the Variable.
   * To read the data in all structures, use ncfile.readSectionSpec().
   *

   * Note this only allows you to specify a subset of this variable.
   * If the variable is nested in a array of structures and you want to subset that, use
   * NetcdfFile.read(String sectionSpec, boolean flatten);
   *
   * @param section list of Range specifying the section of data to read.
   *                Must be null or same rank as variable.
   *                If list is null, assume all data.
   *                Each Range corresponds to a Dimension. If the Range object is null, it means use the entire dimension.
   * @return the requested data in a memory-resident Array
   * @throws IOException           if error
   * @throws InvalidRangeException if section is invalid
   */
  public Array read(ucar.ma2.Section section) throws java.io.IOException, ucar.ma2.InvalidRangeException {
    return (section == null) ? _read() : _read(Section.fill(section, shape));
  }

  /**
   * Read all the data for this Variable and return a memory resident Array.
   * The Array has the same element type and shape as the Variable.
   *

   * If the Variable is a member of an array of Structures, this returns only the variable's data
   * in the first Structure, so that the Array shape is the same as the Variable.
   * To read the data in all structures, use ncfile.readSection().
   *
   * @return the requested data in a memory-resident Array.
   */
  public Array read() throws IOException {
    return _read();
  }

  /**
   * *********************************************************************
   */
  // scalar reading

  /**
   * Get the value as a byte for a scalar Variable. May also be one-dimensional of length 1.
   *
   * @throws IOException                   if theres an IO Error
   * @throws UnsupportedOperationException if not a scalar Variable or one-dimensional of length 1.
   * @throws ForbiddenConversionException  if data type not convertible to byte
   */
  public byte readScalarByte() throws IOException {
    Array data = getScalarData();
    return data.getByte(Index.scalarIndexImmutable);
  }

  /**
   * Get the value as a short for a scalar Variable.  May also be one-dimensional of length 1.
   *
   * @throws IOException                   if theres an IO Error
   * @throws UnsupportedOperationException if not a scalar Variable  or one-dimensional of length 1.
   * @throws ForbiddenConversionException  if data type not convertible to short
   */
  public short readScalarShort() throws IOException {
    Array data = getScalarData();
    return data.getShort(Index.scalarIndexImmutable);
  }

  /**
   * Get the value as a int for a scalar Variable. May also be one-dimensional of length 1.
   *
   * @throws IOException                   if theres an IO Error
   * @throws UnsupportedOperationException if not a scalar Variable or one-dimensional of length 1.
   * @throws ForbiddenConversionException  if data type not convertible to int
   */
  public int readScalarInt() throws IOException {
    Array data = getScalarData();
    return data.getInt(Index.scalarIndexImmutable);
  }

  /**
   * Get the value as a long for a scalar Variable.  May also be one-dimensional of length 1.
   *
   * @throws IOException                   if theres an IO Error
   * @throws UnsupportedOperationException if not a scalar Variable
   * @throws ForbiddenConversionException  if data type not convertible to long
   */
  public long readScalarLong() throws IOException {
    Array data = getScalarData();
    return data.getLong(Index.scalarIndexImmutable);
  }

  /**
   * Get the value as a float for a scalar Variable.  May also be one-dimensional of length 1.
   *
   * @throws IOException                   if theres an IO Error
   * @throws UnsupportedOperationException if not a scalar Variable or one-dimensional of length 1.
   * @throws ForbiddenConversionException  if data type not convertible to float
   */
  public float readScalarFloat() throws IOException {
    Array data = getScalarData();
    return data.getFloat(Index.scalarIndexImmutable);
  }

  /**
   * Get the value as a double for a scalar Variable.  May also be one-dimensional of length 1.
   *
   * @throws IOException                   if theres an IO Error
   * @throws UnsupportedOperationException if not a scalar Variable or one-dimensional of length 1.
   * @throws ForbiddenConversionException  if data type not convertible to double
   */
  public double readScalarDouble() throws IOException {
    Array data = getScalarData();
    return data.getDouble(Index.scalarIndexImmutable);
  }

  /**
   * Get the value as a String for a scalar Variable.  May also be one-dimensional of length 1.
   * May also be one-dimensional of type CHAR, which wil be turned into a scalar String.
   *
   * @throws IOException                   if theres an IO Error
   * @throws UnsupportedOperationException if not a scalar or one-dimensional.
   * @throws ClassCastException            if data type not DataType.STRING or DataType.CHAR.
   */
  public String readScalarString() throws IOException {
    Array data = getScalarData();
    if (dataType == DataType.STRING)
      return (String) data.getObject(Index.scalarIndexImmutable);
    else if (dataType == DataType.CHAR) {
      ArrayChar dataC = (ArrayChar) data;
      return dataC.getString();
    } else
      throw new IllegalArgumentException("readScalarString not STRING or CHAR " + getFullName());
  }

  protected Array getScalarData() throws IOException {
    Array scalarData = (cache.data != null) ? cache.data : read();
    scalarData = scalarData.reduce();

    if ((scalarData.getRank() == 0) || ((scalarData.getRank() == 1) && dataType == DataType.CHAR))
      return scalarData;
    throw new java.lang.UnsupportedOperationException("not a scalar variable =" + this);
  }

  ///////////////
  // internal reads: all other calls go through these.
  // subclasses must override, so that NetcdfDataset wrapping will work.

  // non-structure-member Variables.

  protected Array _read() throws IOException {
    // caching overrides the proxyReader
    // check if already cached
    if (cache.data != null) {
      if (debugCaching) System.out.println("got data from cache " + getFullName());
      return cache.data.copy();
    }

    Array data = proxyReader.reallyRead(this, null);

    // optionally cache it
    if (isCaching()) {
      setCachedData(data);
      if (debugCaching) System.out.println("cache " + getFullName());
      return cache.data.copy(); // dont let users get their nasty hands on cached data
    } else {
      return data;
    }
  }

  /**
   * public by accident, do not call directly.
   *
   * @return Array
   * @throws IOException on error
   */
  @Override
  public Array reallyRead(Variable client, CancelTask cancelTask) throws IOException {
    if (isMemberOfStructure()) { // LOOK should be UnsupportedOperationException ??
      List<String> memList = new ArrayList<>();
      memList.add(this.getShortName());
      Structure s = getParentStructure().select(memList);
      ArrayStructure as = (ArrayStructure) s.read();
      return as.extractMemberArray(as.findMember(getShortName()));
    }

    try {
      return ncfile.readData(this, getShapeAsSection());
    } catch (InvalidRangeException e) {
      e.printStackTrace();
      throw new IOException(e.getMessage()); // cant happen haha
    }
  }

  // section of non-structure-member Variable
  // assume filled, validated Section
  protected Array _read(Section section) throws IOException, InvalidRangeException {
    // check if its really a full read
    if ((null == section) || section.computeSize() == getSize())
      return _read();

    // full read was cached
    if (isCaching()) {
      if (cache.data == null) {
        setCachedData(_read()); // read and cache entire array
        if (debugCaching) System.out.println("cache " + getFullName());
      }
      if (debugCaching) System.out.println("got data from cache " + getFullName());
      return cache.data.sectionNoReduce(section.getRanges()).copy(); // subset it, return copy
    }

    return proxyReader.reallyRead(this, section, null);
  }

  /**
   * public by accident, do not call directly.
   *
   * @return Array
   * @throws IOException on error
   */
  @Override
  public Array reallyRead(Variable client, Section section, CancelTask cancelTask) throws IOException, InvalidRangeException {
    if (isMemberOfStructure()) {
      throw new UnsupportedOperationException("Cannot directly read section of Member Variable=" + getFullName());
    }
    // read just this section
    return ncfile.readData(this, section);
  }

  /* structure-member Variable;  section has a Range for each array in the parent
  // stuctures(s) and for the Variable.
  private Array _readMemberData(List<Range> section, boolean flatten) throws IOException, InvalidRangeException {
    /*Variable useVar = (ioVar != null) ? ioVar : this;
    NetcdfFile useFile = (ncfileIO != null) ? ncfileIO : ncfile;
    return useFile.readMemberData(useVar, section, flatten);
  } */

  public long readToByteChannel(Section section, WritableByteChannel wbc) throws IOException, InvalidRangeException {
    if ((ncfile == null) || hasCachedData())
      return IospHelper.copyToByteChannel(read(section), wbc);

    return ncfile.readToByteChannel(this, section, wbc);
  }

  public long readToStream(Section section, OutputStream out) throws IOException, InvalidRangeException {
    if ((ncfile == null) || hasCachedData())
      return IospHelper.copyToOutputStream(read(section), out);

    return ncfile.readToOutputStream(this, section, out);
  }

  /*******************************************/
  /* nicely formatted string representation */

  /**
   * Get the display name plus the dimensions, eg 'float name(dim1, dim2)'
   *
   * @return display name plus the dimensions
   */
  public String getNameAndDimensions() {
    Formatter buf = new Formatter();
    getNameAndDimensions(buf, true, false);
    return buf.toString();
  }

  /**
   * Get the display name plus the dimensions, eg 'float name(dim1, dim2)'
   *
   * @param strict strictly comply with ncgen syntax, with name escaping. otherwise, get extra info, no escaping
   * @return display name plus the dimensions
   */
  public String getNameAndDimensions(boolean strict) {
    Formatter buf = new Formatter();
    getNameAndDimensions(buf, false, strict);
    return buf.toString();
  }

  /**
   * Get the display name plus the dimensions, eg 'name(dim1, dim2)'
   *
   * @param buf add info to this StringBuilder
   */
  public void getNameAndDimensions(StringBuilder buf) {
    getNameAndDimensions(buf, true, false);
  }

  /**
   * Get the display name plus the dimensions, eg 'name(dim1, dim2)'
   *
   * @param buf add info to this StringBuffer
   * @deprecated use getNameAndDimensions(StringBuilder buf)
   */
  public void getNameAndDimensions(StringBuffer buf) {
    Formatter proxy = new Formatter();
    getNameAndDimensions(proxy, true, false);
    buf.append(proxy.toString());
  }

  /**
   * Add display name plus the dimensions to the StringBuffer
   *
   * @param buf         add info to this
   * @param useFullName use full name else short name. strict = true implies short name
   * @param strict      strictly comply with ncgen syntax, with name escaping. otherwise, get extra info, no escaping
   */
  public void getNameAndDimensions(StringBuilder buf, boolean useFullName, boolean strict) {
    Formatter proxy = new Formatter();
    getNameAndDimensions(proxy, useFullName, strict);
    buf.append(proxy.toString());
  }

  /**
   * Add display name plus the dimensions to the StringBuffer
   *
   * @param buf         add info to this
   * @param useFullName use full name else short name. strict = true implies short name
   * @param strict      strictly comply with ncgen syntax, with name escaping. otherwise, get extra info, no escaping
   */
  public void getNameAndDimensions(Formatter buf, boolean useFullName, boolean strict) {
    useFullName = useFullName && !strict;
    String name = useFullName ? getFullName() : getShortName();
    if (strict) name = NetcdfFile.makeValidCDLName(getShortName());
    buf.format("%s", name);

    if (shape != null) {
      if (getRank() > 0) buf.format("(");
      for (int i = 0; i < dimensions.size(); i++) {
        Dimension myd = dimensions.get(i);
        String dimName = myd.getShortName();
        if ((dimName != null) && strict)
          dimName = NetcdfFile.makeValidCDLName(dimName);
        if (i != 0) buf.format(", ");
        if (myd.isVariableLength()) {
          buf.format("*");
        } else if (myd.isShared()) {
          if (!strict)
            buf.format("%s=%d", dimName, myd.getLength());
          else
            buf.format("%s", dimName);
        } else {
          if (dimName != null) {
            buf.format("%s=", dimName);
          }
          buf.format("%d", myd.getLength());
        }
      }
      if (getRank() > 0) buf.format(")");
    }
  }

  /**
   * CDL representation of Variable, not strict.
   */
  public String toString() {
    return writeCDL(false, false);
  }

  /**
   * CDL representation of a Variable.
   *
   * @param useFullName use full name, else use short name
   * @param strict      strictly comply with ncgen syntax
   * @return CDL representation of the Variable.
   */
  public String writeCDL(boolean useFullName, boolean strict) {
    Formatter buf = new Formatter();
    writeCDL(buf, new Indent(2), useFullName, strict);
    return buf.toString();
  }

  protected void writeCDL(Formatter buf, Indent indent, boolean useFullName, boolean strict) {
    buf.format("%s", indent);
    if (dataType == null)
      buf.format("Unknown");
    else if (dataType.isEnum()) {
      if (enumTypedef == null)
        buf.format("enum UNKNOWN");
      else
        buf.format("enum %s", NetcdfFile.makeValidCDLName(enumTypedef.getShortName()));
    } else
      buf.format("%s", dataType.toString());

    //if (isVariableLength) buf.append("(*)"); // LOOK
    buf.format(" ");
    getNameAndDimensions(buf, useFullName, strict);
    buf.format(";");
    if (!strict) buf.format(extraInfo());
    buf.format("%n");

    indent.incr();
    for (Attribute att : getAttributes()) {
      if(Attribute.isspecial(att)) continue;
      buf.format("%s", indent);
      att.writeCDL(buf, strict, getShortName());
      buf.format(";");
      if (!strict && (att.getDataType() != DataType.STRING))
        buf.format(" // %s", att.getDataType());
      buf.format("%n");
    }
    indent.decr();
  }

  /**
   * String representation of Variable and its attributes.
   */
  public String toStringDebug() {
    Formatter f = new Formatter();
    f.format("Variable %s", getFullName());
    if (ncfile != null) {
      f.format(" in file %s", getDatasetLocation());
      String extra = ncfile.toStringDebug(this);
      if (extra != null)
        f.format(" %s", extra);
    }
    return f.toString();
  }

  private static boolean showSize = false;

  protected String extraInfo() {
    return showSize ? " // " + getElementSize() + " " + getSize() : "";
  }

  public String getDatasetLocation() {
    if (ncfile != null) return ncfile.getLocation();
    return "N/A";
  }

  /**
   * Instances which have same content are equal.
   */
  public boolean equals(Object oo) {
    if (this == oo) return true;
    if (!(oo instanceof Variable)) return false;
    Variable o = (Variable) oo;

    if (!getShortName().equals(o.getShortName())) return false;
    if (isScalar() != o.isScalar()) return false;
    if (getDataType() != o.getDataType()) return false;
    if (!getParentGroup().equals(o.getParentGroup())) return false;
    if ((getParentStructure() != null) && !getParentStructure().equals(o.getParentStructure())) return false;
    if (isVariableLength() != o.isVariableLength()) return false;
    if (dimensions.size() != o.getDimensions().size()) return false;
    for (int i = 0; i < dimensions.size(); i++)
      if (!getDimension(i).equals(o.getDimension(i))) return false;

    return true;
  }

  /**
   * Override Object.hashCode() to implement equals.
   */
  @Override
  public int hashCode() {
    if (hashCode == 0) {
      int result = 17;
      result = 37 * result + getShortName().hashCode();
      if (isScalar()) result++;
      result = 37 * result + getDataType().hashCode();
      result = 37 * result + getParentGroup().hashCode();
      if (getParentStructure() != null)
        result = 37 * result + getParentStructure().hashCode();
      if (isVariableLength) result++;
      result = 37 * result + dimensions.hashCode();
      hashCode = result;
    }
    return hashCode;
  }

  public void hashCodeShow(Indent indent) {
    System.out.printf("%sVar hash = %d%n", indent, hashCode());
    System.out.printf("%s shortName %s = %d%n", indent, getShortName(), getShortName().hashCode());
    System.out.printf("%s isScalar %s%n", indent, isScalar());
    System.out.printf("%s dataType %s%n", indent, getDataType());
    System.out.printf("%s parentGroup %s = %d%n", indent, getParentGroup(), getParentGroup().hashCode());
    System.out.printf("%s isVariableLength %s%n", indent, isVariableLength);
    System.out.printf("%s dimensions %d len=%d%n", indent, dimensions.hashCode(), dimensions.size());
    indent.incr();
    for (Dimension d : dimensions) {
      d.hashCodeShow(indent);
    }
    indent.decr();
    if (getParentStructure() != null) {
      System.out.printf("%s parentStructure %d%n", indent, getParentStructure().hashCode());
      getParentStructure().hashCodeShow(indent.incr());
      indent.decr();
    }
  }

  protected int hashCode = 0;

  /**
   * Sort by name
   */
  public int compareTo(VariableSimpleIF o) {
    return getShortName().compareTo(o.getShortName());
  }

  /////////////////////////////////////////////////////////////////////////////

  protected Variable() {
  }

  /**
   * Create a Variable. Also must call setDataType() and setDimensions()
   *
   * @param ncfile    the containing NetcdfFile.
   * @param group     the containing group; if null, use rootGroup
   * @param parent    parent Structure, may be null
   * @param shortName variable shortName, must be unique within the Group
   */
  public Variable(NetcdfFile ncfile, Group group, Structure parent, String shortName) {
    super(shortName);
    this.ncfile = ncfile;
    if (parent == null)
      setParentGroup((group == null) ? ncfile.getRootGroup() : group);
    else
      setParentStructure(parent);
    attributes = new AttributeContainerHelper(shortName);
  }

  /**
   * Create a Variable. Also must call setDataType() and setDimensions()
   *
   * @param ncfile    the containing NetcdfFile.
   * @param group     the containing group; if null, use rootGroup
   * @param parent    parent Structure, may be null
   * @param shortName variable shortName, must be unique within the Group
   * @param dtype     the Variable's DataType
   * @param dims      space delimited list of dimension names. may be null or "" for scalars.
   */
  public Variable(NetcdfFile ncfile, Group group, Structure parent, String shortName, DataType dtype, String dims) {
    this(ncfile, group, parent, shortName, dtype, (List<Dimension>)null);
    if(group == null)
      group = ncfile.getRootGroup();
    setDimensions(Dimension.makeDimensionsList(group, dims));
  }

  /**
   * Create a Variable. Also must call setDataType() and setDimensions()
   *
   * @param ncfile    the containing NetcdfFile.
   * @param group     the containing group; if null, use rootGroup
   * @param parent    parent Structure, may be null
   * @param shortName variable shortName, must be unique within the Group
   * @param dtype     the Variable's DataType
   * @param dims      dimension names.
   */
  public Variable(NetcdfFile ncfile, Group group, Structure parent,
                  String shortName, DataType dtype, List<Dimension> dims) {
    this(ncfile, group, parent, shortName);
    setDataType(dtype);
    setDimensions(dims);
  }

  /**
   * Copy constructor.
   * The returned Variable is mutable.
   * It shares the cache object and the iosp Object, attributes and dimensions with the original.
   * Does not share the proxyReader.
   * Use for section, slice, "logical views" of original variable.
   *
   * @param from copy from this Variable.
   */
  public Variable(Variable from) {
    super(from.getShortName());
    this.attributes = new AttributeContainerHelper(from.getShortName(), from.getAttributes());
    this.cache = from.cache; // caller should do createNewCache() if dont want to share
    setDataType(from.getDataType());
    this.dimensions = new ArrayList<>(from.dimensions); // dimensions are shared
    this.elementSize = from.getElementSize();
    this.enumTypedef = from.enumTypedef;
    setParentGroup(from.group);
    setParentStructure(from.getParentStructure());
    this.isMetadata = from.isMetadata;
    this.isVariableLength = from.isVariableLength;
    this.ncfile = from.ncfile;
    this.shape = from.getShape();
    this.sizeToCache = from.sizeToCache;
    this.spiObject = from.spiObject;
  }

  ///////////////////////////////////////////////////
  // the following make this mutable

  /**
   * Set the data type
   *
   * @param dataType set to this value
   */
  public void setDataType(DataType dataType) {
    if (immutable) throw new IllegalStateException("Cant modify");
    this.dataType = dataType;
    this.elementSize = getDataType().getSize();

    /* why is this needed ??
    EnumTypedef etd = getEnumTypedef();
    if (etd != null) {
      DataType etdtype = etd.getBaseType();
      if (dataType != etdtype)
        log.error("Variable.setDataType: enum basetype mismatch: {} != {}", etdtype, dataType);

      /* DataType basetype = null;
      if (dataType == DataType.ENUM1) basetype = DataType.BYTE;
      else if (dataType == DataType.ENUM2) basetype = DataType.SHORT;
      else if (dataType == DataType.ENUM4) basetype = DataType.INT;
      else basetype = etdtype;

      if (etdtype != null && dataType != etdtype)
      else
        etd.setBaseType(basetype);
    }  */
  }

  /**
   * Set the short name, converting to valid CDM object name if needed.
   *
   * @param shortName set to this value
   * @return valid CDM object name
   */
  public String setName(String shortName) {
    if (immutable) throw new IllegalStateException("Cant modify");
    setShortName(shortName);
    return getShortName();
  }

  /**
   * Set the parent group.
   *
   * @param group set to this value
   */
  public void setParentGroup(Group group) {
    if (immutable) throw new IllegalStateException("Cant modify");
    super.setParentGroup(group);
  }

  /**
   * Set the element size. Usually elementSize is determined by the dataType,
   * use this only for exceptional cases.
   *
   * @param elementSize set to this value
   */
  public void setElementSize(int elementSize) {
    if (immutable) throw new IllegalStateException("Cant modify");
    this.elementSize = elementSize;
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // AttributeHelper

  public java.util.List<Attribute> getAttributes() {
    return attributes.getAttributes();
  }

  public AttributeContainer getAttributeContainer() {
    return new AttributeContainerHelper(getFullName(), attributes.getAttributes());
  }

  public Attribute findAttribute(String name) {
    return attributes.findAttribute(name);
  }

  public Attribute findAttributeIgnoreCase(String name) {
    return attributes.findAttributeIgnoreCase(name);
  }

  public String findAttValueIgnoreCase(String attName, String defaultValue) {
    return attributes.findAttValueIgnoreCase(attName, defaultValue);
  }

  public Attribute addAttribute(Attribute att) {
    return attributes.addAttribute(att);
  }

  public void addAll(Iterable<Attribute> atts) {
    attributes.addAll(atts);
  }

  public boolean remove(Attribute a) {
    return attributes.remove(a);
  }

  public boolean removeAttribute(String attName) {
    return attributes.removeAttribute(attName);
  }

  public boolean removeAttributeIgnoreCase(String attName) {
    return attributes.removeAttributeIgnoreCase(attName);
  }

  ////////////////////////////////////////////////////////////////////////

  /**
   * Set the shape with a list of Dimensions. The Dimensions may be shared or not.
   * Dimensions are in order, slowest varying first. Send a null for a scalar.
   * Technically you can use Dimensions from any group; pragmatically you should only use
   * Dimensions contained in the Variable's parent groups.
   *
   * @param dims list of type ucar.nc2.Dimension
   */
  public void setDimensions(List<Dimension> dims) {
    if (immutable) throw new IllegalStateException("Cant modify");
    this.dimensions = (dims == null) ? new ArrayList<>() : new ArrayList<>(dims);
    resetShape();
  }

  /**
   * Use when dimensions have changed, to recalculate the shape.
   */
  public void resetShape() {
    // if (immutable) throw new IllegalStateException("Cant modify");  LOOK allow this for unlimited dimension updating
    this.shape = new int[dimensions.size()];
    for (int i = 0; i < dimensions.size(); i++) {
      Dimension dim = dimensions.get(i);
      shape[i] = dim.getLength();
      //shape[i] = Math.max(dim.getLength(), 0); // LOOK
      // if (dim.isUnlimited() && (i != 0)) // LOOK only true for Netcdf-3
      //   throw new IllegalArgumentException("Unlimited dimension must be outermost");
      if (dim.isVariableLength()) {
        //if (dimensions.size() != 1)
        //  throw new IllegalArgumentException("Unknown dimension can only be used in 1 dim array");
        //else
        isVariableLength = true;
      }
    }
    this.shapeAsSection = null; // recalc next time its asked for
  }

  /**
   * Set the dimensions using the dimensions names. The dimension is searched for recursively in the parent groups.
   *
   * @param dimString : whitespace separated list of dimension names, or '*' for Dimension.UNKNOWN, or number for anon dimension. null or empty String is a scalar.
   */
  public void setDimensions(String dimString) {
    if (immutable) throw new IllegalStateException("Cant modify");
    try {
      setDimensions(Dimension.makeDimensionsList(getParentGroup(), dimString));
      //this.dimensions = Dimension.makeDimensionsList(getParentGroup(), dimString);
      resetShape();
    } catch (IllegalStateException e) {
      throw new IllegalArgumentException("Variable " + getFullName() + " setDimensions = '" + dimString +
              "' FAILED: " + e.getMessage() + " file = " + getDatasetLocation());
    }
  }

  /**
   * Reset the dimension array. Anonymous dimensions are left alone.
   * Shared dimensions are searched for recursively in the parent groups.
   */
  public void resetDimensions() {
    if (immutable) throw new IllegalStateException("Cant modify");
    ArrayList<Dimension> newDimensions = new ArrayList<>();

    for (Dimension dim : dimensions) {
      if (dim.isShared()) {
        Dimension newD = getParentGroup().findDimension(dim.getShortName());
        if (newD == null)
          throw new IllegalArgumentException("Variable " + getFullName() + " resetDimensions  FAILED, dim doesnt exist in parent group=" + dim);
        newDimensions.add(newD);
      } else {
        newDimensions.add(dim);
      }
    }
    this.dimensions = newDimensions;
    resetShape();
  }

  /**
   * Set the dimensions using all anonymous (unshared) dimensions
   *
   * @param shape defines the dimension lengths. must be > 0, or -1 for VLEN
   * @throws ucar.ma2.InvalidRangeException if any shape < 1
   */
  public void setDimensionsAnonymous(int[] shape) throws InvalidRangeException {
    if (immutable) throw new IllegalStateException("Cant modify");
    this.dimensions = new ArrayList<>();
    for (int i = 0; i < shape.length; i++) {
      if ((shape[i] < 1) && (shape[i] != -1))
        throw new InvalidRangeException("shape[" + i + "]=" + shape[i] + " must be > 0");
      Dimension anon;
      if (shape[i] == -1) {
        anon = Dimension.VLEN;
        isVariableLength = true;
      } else {
        anon = new Dimension(null, shape[i], false, false, false);
      }

      dimensions.add(anon);
    }
    resetShape();
  }

  /**
   * Set this Variable to be a scalar
   */
  public void setIsScalar() {
    if (immutable) throw new IllegalStateException("Cant modify");
    this.dimensions = new ArrayList<>();
    resetShape();
  }

  /**
   * Replace a dimension with an equivalent one.
   * @param dim must have the same name, length as old one
   *
  public void replaceDimension( Dimension dim) {
  int idx = findDimensionIndex( dim.getName());
  if (idx >= 0)
  dimensions.set( idx, dim);
  resetShape();
  } */

  /**
   * Replace a dimension with an equivalent one.
   *
   * @param idx index into dimension array
   * @param dim to set
   */
  public void setDimension(int idx, Dimension dim) {
    if (immutable) throw new IllegalStateException("Cant modify");
    dimensions.set(idx, dim);
    resetShape();
  }

  /**
   * Make this immutable.
   *
   * @return this
   */
  public Variable setImmutable() {
    super.setImmutable();
    dimensions = Collections.unmodifiableList(dimensions);
    attributes.setImmutable();
    return this;
  }

  /**
   * Is this Variable immutable
   *
   * @return if immutable
   */
  public boolean isImmutable() {
    return immutable;
  }

  // for IOServiceProvider
  protected Object spiObject;

  /**
   * Should not be public.
   *
   * @return the IOSP object
   */
  public Object getSPobject() {
    return spiObject;
  }

  /**
   * Should not be public.
   *
   * @param spiObject the IOSP object
   */
  public void setSPobject(Object spiObject) {
    this.spiObject = spiObject;
  }

  ////////////////////////////////////////////////////////////////////////////////////
  // caching

  /**
   * If total data size is less than SizeToCache in bytes, then cache.
   *
   * @return size at which caching happens
   */
  public int getSizeToCache() {
    if (sizeToCache >= 0) return sizeToCache; // it was set
    return isCoordinateVariable() ? defaultCoordsSizeToCache : defaultSizeToCache;
  }

  /**
   * Set the sizeToCache. If not set, use defaults
   *
   * @param sizeToCache size at which caching happens. < 0 means use defaults
   */
  public void setSizeToCache(int sizeToCache) {
    this.sizeToCache = sizeToCache;
  }

  /**
   * Set whether to cache or not. Implies that the entire array will be stored, once read.
   * Normally this is set automatically based on size of data.
   *
   * @param caching set if caching.
   */
  public void setCaching(boolean caching) {
    this.cache.isCaching = caching;
    this.cache.cachingSet = true;
  }

  /**
   * Will this Variable be cached when read.
   * Set externally, or calculated based on total size < sizeToCache.
   *

   * This will always return {@code false} if {@link #permitCaching caching isn't permitted}.
   *
   * @return true is caching
   */
  public boolean isCaching() {
    if (!permitCaching) {
      return false;
    }

    if (!this.cache.cachingSet) {
      cache.isCaching = !isVariableLength && (getSize() * getElementSize() < getSizeToCache());
      if (debugCaching) System.out.printf("  cache %s %s %d < %d%n", getFullName(), cache.isCaching, getSize() * getElementSize(), getSizeToCache());
      this.cache.cachingSet = true;
    }
    return cache.isCaching;
  }

  /**
   * Invalidate the data cache
   */
  public void invalidateCache() {
    cache.data = null;
  }

  public void setCachedData(Array cacheData) {
    setCachedData(cacheData, false);
  }

  //public Array getCachedData() {
  //  return (cache == null) ? null : cache.data;
  //}

  /**
   * Set the data cache
   *
   * @param cacheData  cache this Array
   * @param isMetadata : synthesized data, set true if must be saved in NcML output (ie data not actually in the file).
   */
  public void setCachedData(Array cacheData, boolean isMetadata) {
    if ((cacheData != null) && (cacheData.getElementType() != getDataType().getPrimitiveClassType()))
      throw new IllegalArgumentException("setCachedData type=" + cacheData.getElementType() + " incompatible with variable type=" + getDataType());

    this.cache.data = cacheData;
    this.isMetadata = isMetadata;
    this.cache.cachingSet = true;
    this.cache.isCaching = true;
  }

  /**
   * Create a new data cache, use this when you dont want to share the cache.
   */
  public void createNewCache() {
    this.cache = new Cache();
  }

  /**
   * Has data been read and cached.
   * Use only on a Variable, not a subclass.
   *
   * @return true if data is read and cached
   */
  public boolean hasCachedData() {
    return (null != cache.data);
  }

  // this indirection allows us to share the cache among the variable's sections and copies

  /**
   * Public by accident.
   */
  static protected class Cache {
    public Array data;
    public boolean isCaching = false;
    public boolean cachingSet = false;

    public Cache() {
    }
  }

  ///////////////////////////////////////////////////////////////////////
  // setting variable data values

  /**
   * Generate the list of values from a starting value and an increment.
   * Will reshape to variable if needed.
   *
   * @param npts  number of values, must = v.getSize()
   * @param start starting value
   * @param incr  increment
   */
  public void setValues(int npts, double start, double incr) {
    if (npts != getSize())
      throw new IllegalArgumentException("bad npts = " + npts + " should be " + getSize());
    Array data = Array.makeArray(getDataType(), npts, start, incr);
    if (getRank() != 1)
      data = data.reshape(getShape());
    setCachedData(data, true);
  }

  /**
   * Set the data values from a list of Strings.
   *
   * @param values list of Strings
   * @throws IllegalArgumentException if values array not correct size, or values wont parse to the correct type
   */
  public void setValues(List<String> values) throws IllegalArgumentException {
    Array data = Array.makeArray(getDataType(),  values);

    if (data.getSize() != getSize())
      throw new IllegalArgumentException("Incorrect number of values specified for the Variable " + getFullName() +
              " needed= " + getSize() + " given=" + data.getSize());

    if (getRank() != 1) // dont have to reshape for rank 1
      data = data.reshape(getShape());

    setCachedData(data, true);
  }

  ////////////////////////////////////////////////////////////////////////
  // StructureMember - could be a subclass, but that has problems

  /**
   * Get list of Dimensions, including parents if any.
   *
   * @return array of Dimension, rank of v plus all parents.
   */
  public List<Dimension> getDimensionsAll() {
    List<Dimension> dimsAll = new ArrayList<>();
    addDimensionsAll(dimsAll, this);
    return dimsAll;
  }

  private void addDimensionsAll(List<Dimension> result, Variable v) {
    if (v.isMemberOfStructure())
      addDimensionsAll(result, v.getParentStructure());

    for (int i = 0; i < v.getRank(); i++)
      result.add(v.getDimension(i));
  }

  public int[] getShapeAll() {
    if (getParentStructure() == null) return getShape();
    List<Dimension> dimAll = getDimensionsAll();
    int[] shapeAll = new int[dimAll.size()];
    for (int i = 0; i < dimAll.size(); i++)
      shapeAll[i] = dimAll.get(i).getLength();
    return shapeAll;
  }

  /*
   * Read data in all structures for this Variable, using a string sectionSpec to specify the section.
   * See readAllStructures(Section section, boolean flatten) method for details.
   *
   * @param sectionSpec specification string, eg "1:2,10,:,1:100:10"
   * @param flatten     if true, remove enclosing StructureData.
   * @return the requested data which has the shape of the request.
   * @see #readAllStructures
   * @deprecated
   *
  public Array readAllStructuresSpec(String sectionSpec, boolean flatten) throws IOException, InvalidRangeException {
    return readAllStructures(new Section(sectionSpec), flatten);
  }

  /*
   * Read data from all structures for this Variable.
   * This is used for member variables whose parent Structure(s) is not a scalar.
   * You must specify a Range for each dimension in the enclosing parent Structure(s).
   * The returned Array will have the same shape as the requested section.
   *

   *
If flatten is false, return nested Arrays of StructureData that correspond to the nested Structures.
   * The innermost Array(s) will match the rank and type of the Variable, but they will be inside Arrays of
   * StructureData.
   *

   *
If flatten is true, remove the Arrays of StructureData that wrap the data, and return an Array of the
   * same type as the Variable. The shape of the returned Array will be an accumulation of all the shapes of the
   * Structures containing the variable.
   *
   * @param sectionAll an array of Range objects, one for each Dimension of the enclosing Structures, as well as
   *                   for the Variable itself. If the list is null, use the full shape for everything.
   *                   If an individual Range is null, use the full shape for that dimension.
   * @param flatten    if true, remove enclosing StructureData. Otherwise, each parent Structure will create a
   *                   StructureData container for the returned data array.
   * @return the requested data which has the shape of the request.
   * @deprecated
   *
  public Array readAllStructures(ucar.ma2.Section sectionAll, boolean flatten) throws java.io.IOException, ucar.ma2.InvalidRangeException {
    Section resolved; // resolve all nulls
    if (sectionAll == null)
      resolved = makeSectionAddParents(null, false); // everything
    else {
      ArrayList<Range> resultAll = new ArrayList<Range>();
      makeSectionWithParents(resultAll, sectionAll.getRanges(), this);
      resolved = new Section(resultAll);
    }

    return _readMemberData(resolved, flatten);
  }

  // recursively create the section (list of Range) array
  private List<Range> makeSectionWithParents(List<Range> result, List<Range> orgSection, Variable v) throws InvalidRangeException {
    List<Range> section = orgSection;

    // do parent stuctures(s) first
    if (v.isMemberOfStructure())
      section = makeSectionWithParents(result, orgSection, v.getParentStructure());

    // process just this variable's subList
    List<Range> myList = section.subList(0, v.getRank());
    Section mySection = new Section(myList, v.getShape());
    result.addAll(mySection.getRanges());

    // return section with this variable's sublist removed
    return section.subList(v.getRank(), section.size());
  } */

  /*
   * Composes this variable's ranges with another list of ranges, adding parent ranges; resolves nulls.
   *
   * @param section   Section of this Variable, same rank as v, may have nulls or be null.
   * @param firstOnly if true, get first parent, else get all parrents.
   * @return Section, rank of v plus parents, no nulls
   * @throws InvalidRangeException if bad
   *
  private Section makeSectionAddParents(Section section, boolean firstOnly) throws InvalidRangeException {
    Section result;
    if (section == null)
      result = new Section(getRanges());
    else
      result = new Section(section.getRanges(), getShape());

    // add parents
    Structure p = getParentStructure();
    while (p != null) {
      Section parentSection = p.getShapeAsSection();
      for (int i = parentSection.getRank() - 1; i >= 0; i--) { // reverse
        Range r = parentSection.getRange(i);
        result.insertRange(0, firstOnly ? new Range(0, 0) : r);
      }
      p = p.getParentStructure();
    }

    return result;
  } */

  /* private Array readMemberOfStructureFlatten(Section section) throws InvalidRangeException, IOException {
    // get through first parents element
    Section sectionAll = makeSectionAddParents(section, true);
    Array data = _readMemberData(sectionAll, true); // flatten

    // remove parent dimensions.
    int n = data.getRank() - getRank();
    for (int i = 0; i < n; i++)
      if (data.getShape()[0] == 1) data = data.reduce(0);
    return data;
  }

  /* structure-member Variable;  section has a Range for each array in the parent
  // stuctures(s) and for the Variable.
  protected Array _readMemberData(Section section, boolean flatten) throws IOException, InvalidRangeException {
    return ncfile.readMemberData(this, section, flatten);
  } */

  ////////////////////////////////

  /**
   * Calculate if this is a classic coordinate variable: has same name as its first dimension.
   * If type char, must be 2D, else must be 1D.
   *
   * @return true if a coordinate variable.
   */
  public boolean isCoordinateVariable() {
    if ((dataType == DataType.STRUCTURE) || isMemberOfStructure()) // Structures and StructureMembers cant be coordinate variables
      return false;

    int n = getRank();
    if (n == 1 && dimensions.size() == 1) {
      Dimension firstd = dimensions.get(0);
      if (getShortName().equals(firstd.getShortName())) { //  : short names match
        return true;
      }
    }
    if (n == 2 && dimensions.size() == 2) {    // two dimensional
      Dimension firstd = dimensions.get(0);
      if (shortName.equals(firstd.getShortName()) &&  // short names match
              (getDataType() == DataType.CHAR)) {         // must be char valued (really a String)
        return true;
      }
    }

    return false;
  }

  /* public Object clone() throws CloneNotSupportedException {
    Variable clone = (Variable) super.clone();

    // Do we need to clone these?
    // protected Cache cache = new Cache();
    // protected int sizeToCache = -1; // bytes

    clone.setParentGroup(group);
    clone.setParentStructure(getParentStructure());
    clone.setProxyReader(clone);
    return clone;
  }   */

  ///////////////////////////////////////////////////////////////////////
  // deprecated

  /**
   * @return isVariableLength()
   * @deprecated use isVariableLength()
   */
  public boolean isUnknownLength() {
    return isVariableLength;
  }
}


/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package ucar.nc2.dt.radial;

import ucar.nc2.*;
import ucar.nc2.dataset.*;
import ucar.nc2.dt.*;
import ucar.nc2.ft.FeatureDatasetFactory;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.time.CalendarDateFormatter;
import ucar.nc2.time.CalendarDateRange;
import ucar.nc2.time.CalendarDateUnit;
import ucar.nc2.units.DateRange;
import ucar.nc2.constants.FeatureType;
import ucar.nc2.util.cache.FileCacheIF;
import ucar.unidata.geoloc.LatLonRect;
import ucar.ma2.DataType;
import ucar.unidata.geoloc.ProjectionRect;

import java.util.*;
import java.io.IOException;

/**
 * Make a NetcdfDataset into a RadialDatasetSweep.
 */
public abstract class AbstractRadialAdapter implements RadialDatasetSweep, FeatureDatasetFactory {
  protected NetcdfDataset netcdfDataset;
  protected String title, desc, location;
  protected Date startDate, endDate;
  protected LatLonRect boundingBox;
  protected List<VariableSimpleIF> dataVariables = new ArrayList<>();
  protected StringBuffer parseInfo = new StringBuffer();

  protected ucar.unidata.geoloc.EarthLocation origin;
  protected HashMap csHash = new HashMap();
  protected ucar.nc2.units.DateUnit dateUnits;
  protected ucar.nc2.time.CalendarDateUnit calDateUnits;
  protected FileCacheIF fileCache;

  public AbstractRadialAdapter() {
  }

  public AbstractRadialAdapter(NetcdfDataset ds) {
    this.netcdfDataset = ds;
    this.location = netcdfDataset.getLocation();

    this.title = netcdfDataset.getTitle();
    if (title == null)
      title = netcdfDataset.findAttValueIgnoreCase(null, "title", null);
    if (desc == null)
      desc = netcdfDataset.findAttValueIgnoreCase(null, "description", null);

    // look for radial data variables
    parseInfo.append("RadialDatasetAdapter look for RadialVariables\n");
    for (Variable var : ds.getVariables()) {
      addRadialVariable(ds, var);
    }
  }

  protected abstract void addRadialVariable(NetcdfDataset ds, Variable var);
  protected abstract RadialVariable makeRadialVariable(NetcdfDataset nds, VariableSimpleIF v, Variable v0);
  protected abstract void setTimeUnits() throws Exception; // reminder for subclasses to set this
  protected abstract void setEarthLocation(); // reminder for subclasses to set this
  protected abstract void setStartDate(); // reminder for subclasses to set this
  protected abstract void setEndDate(); // reminder for subclasses to set this

  public void setTitle(String title) {
    this.title = title;
  }

  public void setDescription(String desc) {
    this.desc = desc;
  }

  public void setLocationURI(String location) {
    this.location = location;
  }

  protected void removeDataVariable(String varName) {
    Iterator iter = dataVariables.iterator();
    while (iter.hasNext()) {
      VariableSimpleIF v = (VariableSimpleIF) iter.next();
      if (v.getShortName().equals(varName))
        iter.remove();
    }
  }

  // you must set EarthLocation before you call this.
  protected void setBoundingBox() {
    LatLonRect largestBB = null;
    // look through all the coord systems
    for (Object o : csHash.values()) {
      RadialCoordSys sys = (RadialCoordSys) o;
      sys.setOrigin(origin);
      LatLonRect bb = sys.getBoundingBox();
      if (largestBB == null)
        largestBB = bb;
      else if (bb != null)
        largestBB.extend(bb);
    }
    boundingBox = largestBB;
  }

  public void calcBounds() throws java.io.IOException {
    setBoundingBox();
    try {
      setTimeUnits();
    } catch (Exception e) {
      throw new IOException(e.getMessage());
    }
    setStartDate();
    setEndDate();
  }

  //////////////////////////////////////////////////////////////////////////

  public Date getStartDate() {
    return startDate;
  }

  public Date getEndDate() {
    return endDate;
  }

  public LatLonRect getBoundingBox() {
    return boundingBox;
  }

  public List<VariableSimpleIF> getDataVariables() {
    return dataVariables;
  }

  public VariableSimpleIF getDataVariable(String shortName) {
    for (VariableSimpleIF s : dataVariables) {
      String ss = s.getShortName();
      if (shortName.equals(ss)) return s;
    }
    return null;
  }

  public RadialDatasetSweep.Type getCommonType() {
    return null;
  }

  public ucar.nc2.units.DateUnit getTimeUnits() {
    return dateUnits;
  }

  public ucar.nc2.time.CalendarDateUnit getCalendarDateUnit() {
    return calDateUnits;
  }

  public ucar.unidata.geoloc.EarthLocation getEarthLocation() {
    return origin;
  }

  /////////////////////////////////////////////
  // FeatureDatasetFactory

  public FeatureType[] getFeatureTypes() {
    return new FeatureType[]{FeatureType.RADIAL};
  }

  // FeatureDataset
  public FeatureType getFeatureType() {
    return FeatureType.RADIAL;
  }

  public DateRange getDateRange() {
    return new DateRange(getStartDate(), getEndDate());
  }

  public CalendarDateRange getCalendarDateRange() {
    return CalendarDateRange.of(getStartDate(), getEndDate());
  }

  public CalendarDate getCalendarDateStart() {
    return CalendarDate.of(getStartDate());
  }

  public CalendarDate getCalendarDateEnd() {
    return CalendarDate.of(getEndDate());
  }

  public void getDetailInfo(Formatter sf) {
    sf.format("%s", getDetailInfo());
  }

  public String getImplementationName() {
    return getClass().getName();
  }

  //////////////////////////////////////////////////
  //  FileCacheable

  @Override
  public synchronized void setFileCache(FileCacheIF fileCache) {
    this.fileCache = fileCache;
  }

  @Override
  public synchronized void close() throws java.io.IOException {
    if (fileCache != null) {
      if (fileCache.release(this)) return;
    }

    try {
      if (netcdfDataset != null) netcdfDataset.close();
    } finally {
      netcdfDataset = null;
    }
  }

  // release any resources like file handles
  public void release() throws IOException {
    if (netcdfDataset != null) netcdfDataset.release();
  }

  // reacquire any resources like file handles
  public void reacquire() throws IOException {
    if (netcdfDataset != null) netcdfDataset.reacquire();
  }

  @Override
  public long getLastModified() {
    return (netcdfDataset != null) ? netcdfDataset.getLastModified() : 0;
  }

  /////////////////////////////////////////////////

  public NetcdfFile getNetcdfFile() {
    return netcdfDataset;
  }

  public String getTitle() {
    return title;
  }

  public String getDescription() {
    return desc;
  }

  public String getLocationURI() {
    return location;
  }

  public String getLocation() {
    return location;
  }

  public List<Attribute> getGlobalAttributes() {
    if (netcdfDataset == null) return new ArrayList<>();
    return netcdfDataset.getGlobalAttributes();
  }

  public Attribute findGlobalAttributeIgnoreCase(String name) {
    if (netcdfDataset == null) return null;
    return netcdfDataset.findGlobalAttributeIgnoreCase(name);
  }

  public String getDetailInfo() {
    StringBuilder sbuff = new StringBuilder();

    sbuff.append(" Radar ID = " + getRadarID() + "\n");
    sbuff.append(" Radar Name = " + getRadarName() + "\n");
    sbuff.append(" Data Format Name= " + getDataFormat() + "\n");
    sbuff.append(" Common Type = " + getCommonType() + "\n");
    sbuff.append(" Common Origin = " + getCommonOrigin() + "\n");
    CalendarDateUnit dt = getCalendarDateUnit();
    if (dt != null)
      sbuff.append(" Date Unit = " + dt + "\n");
    sbuff.append(" isStationary = " + isStationary() + "\n");
    //sbuff.append(" isRadial = "+isRadial()+"\n");
    sbuff.append(" isVolume = " + isVolume() + "\n");
    sbuff.append("\n");

    sbuff.append("  location= ").append(getLocation()).append("\n");
    sbuff.append("  title= ").append(getTitle()).append("\n");
    sbuff.append("  desc= ").append(getDescription()).append("\n");
    sbuff.append("  start= ").append(CalendarDateFormatter.toDateTimeString(getStartDate())).append("\n");
    sbuff.append("  end  = ").append(CalendarDateFormatter.toDateTimeString(getEndDate())).append("\n");
    sbuff.append("  bb   = ").append(getBoundingBox()).append("\n");
    if (getBoundingBox() != null)
      sbuff.append("  bb   = ").append(getBoundingBox().toString2()).append("\n");

    sbuff.append("  has netcdf = ").append(getNetcdfFile() != null).append("\n");
    List<Attribute> ga = getGlobalAttributes();
    if (ga.size() > 0) {
      sbuff.append("  Attributes\n");
      for (Attribute a : ga) {
        sbuff.append("    ").append(a).append("\n");
      }
    }

    List<VariableSimpleIF> vars = getDataVariables();
    sbuff.append("  Variables (").append(vars.size()).append(")\n");
    for (VariableSimpleIF v : vars) {
      sbuff.append("    name='").append(v.getShortName()).append("' desc='").append(v.getDescription()).append("' units='").append(v.getUnitsString()).append("' type=").append(v.getDataType()).append("\n");
    }

    sbuff.append("\nparseInfo=\n");
    sbuff.append(parseInfo);
    sbuff.append("\n");

    return sbuff.toString();
  }

  ///////////////////////////////////////////////////////////////////////
  public class MyRadialVariableAdapter implements VariableSimpleIF {

    private int rank;
    private int[] shape;
    private String name;
    private String desp;
    private List<Attribute> attributes;

    public MyRadialVariableAdapter(String vName, List<Attribute> atts) {
      super();
      rank = 1;
      shape = new int[]{1};
      name = vName;
      desp = "A radial variable holding a list of radial sweeps";
      attributes = atts;
    }

    public String toString() {
      return name;
    }

    /**
     * Sort by name
     */
    public int compareTo(VariableSimpleIF o) {
      return getFullName().compareTo(o.getFullName());
    }

    public String getName() {
      return this.name;
    }

    public String getFullName() {
      return this.name;
    }

    public String getShortName() {
      return this.name;
    }

    public DataType getDataType() {
      return DataType.FLOAT;
    }

    public String getDescription() {
      return this.desp;
    }

    public String getInfo() {
      return this.desp;
    }

    public String getUnitsString() {
      return "N/A";
    }

    public int getRank() {
      return this.rank;
    }

    public int[] getShape() {
      return this.shape;
    }

    public List<Dimension> getDimensions() {
      return null;
    }

    public List<Attribute> getAttributes() {
      return attributes;
    }

    public ucar.nc2.Attribute findAttributeIgnoreCase(String attName) {
      Iterator it = attributes.iterator();
      Attribute at = null;
      while (it.hasNext()) {
        at = (Attribute) it.next();
        if (attName.equalsIgnoreCase(at.getShortName()))
          break;
      }
      return at;
    }
  }

}