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

/**
 * The Swap class provides static methods for swapping the bytes of chars,
 * shorts, ints, longs, floats, and doubles.
 *
 * @author Kirk Waters
 * @author Russ Rew, 1998, added documentation
 */

public class Swap {

  /**
   * Returns the short resulting from swapping 2 bytes at a specified
   * offset in a byte array.
   *
   * @param b      the byte array
   * @param offset the offset of the first byte
   * @return the short represented by the bytes
   *         b[offset+1], b[offset]
   */
  static public short swapShort(byte[] b, int offset) {
    // 2 bytes
    int low = b[offset] & 0xff;
    int high = b[offset + 1] & 0xff;
    return (short) (high << 8 | low);
  }

  /**
   * Returns the int resulting from reversing 4 bytes at a specified
   * offset in a byte array.
   *
   * @param b      the byte array
   * @param offset the offset of the first byte
   * @return the int represented by the bytes
   *         b[offset+3], b[offset+2], ..., b[offset]
   */
  static public int swapInt(byte[] b, int offset) {
    // 4 bytes
    int accum = 0;
    for (int shiftBy = 0, i = offset; shiftBy < 32; shiftBy += 8, i++)
      accum |= (b[i] & 0xff) << shiftBy;
    return accum;
  }

  /**
   * Returns the long resulting from reversing 8 bytes at a specified
   * offset in a byte array.
   *
   * @param b      the byte array
   * @param offset the offset of the first byte
   * @return the long represented by the bytes
   *         b[offset+7], b[offset+6], ..., b[offset]
   */
  static public long swapLong(byte[] b, int offset) {
    // 8 bytes
    long accum = 0;
    long shiftedval;
    for (int shiftBy = 0, i = offset; shiftBy < 64; shiftBy += 8, i++) {
      shiftedval = ((long) (b[i] & 0xff)) << shiftBy;
      accum |= shiftedval;
    }
    return accum;
  }

  /**
   * Returns the float resulting from reversing 4 bytes at a specified
   * offset in a byte array.
   *
   * @param b      the byte array
   * @param offset the offset of the first byte
   * @return the float represented by the bytes
   *         b[offset+3], b[offset+2], ..., b[offset]
   */
  static public float swapFloat(byte[] b, int offset) {
    int accum = 0;
    for (int shiftBy = 0, i = offset; shiftBy < 32; shiftBy += 8, i++)
      accum |= (b[i] & 0xff) << shiftBy;
    return Float.intBitsToFloat(accum);
  }

  /**
   * Returns the double resulting from reversing 8 bytes at a specified
   * offset in a byte array.
   *
   * @param b      the byte array
   * @param offset the offset of the first byte
   * @return the double represented by the bytes
   *         b[offset+7], b[offset+6], ..., b[offset]
   */
  static public double swapDouble(byte[] b, int offset) {
    long accum = 0;
    long shiftedval;
    for (int shiftBy = 0, i = offset; shiftBy < 64; shiftBy += 8, i++) {
      shiftedval = ((long) (b[i] & 0xff)) << shiftBy;
      accum |= shiftedval;
    }
    return Double.longBitsToDouble(accum);
  }

  /**
   * Returns the char resulting from swapping 2 bytes at a specified
   * offset in a byte array.
   *
   * @param b      the byte array
   * @param offset the offset of the first byte
   * @return the char represented by the bytes
   *         b[offset+1], b[offset]
   */
  static public char swapChar(byte[] b, int offset) {
    // 2 bytes
    int low = b[offset] & 0xff;
    int high = b[offset + 1] & 0xff;
    return (char) (high << 8 | low);
  }

  /**
   * Returns the short resulting from swapping 2 bytes of a specified
   * short.
   *
   * @param s input value for which byte reversal is desired
   * @return the value represented by the bytes of s
   *         reversed
   */
  static public short swapShort(short s) {
    return (swapShort(shortToBytes(s), 0));
  }

  /**
   * Returns the int resulting from reversing 4 bytes of a specified
   * int.
   *
   * @param v input value for which byte reversal is desired
   * @return the value represented by the bytes of v
   *         reversed
   */
  static public int swapInt(int v) {
    return (swapInt(intToBytes(v), 0));
  }

  /**
   * Returns the long resulting from reversing 8 bytes of a specified
   * long.
   *
   * @param l input value for which byte reversal is desired
   * @return the value represented by the bytes of l
   *         reversed
   */
  static public long swapLong(long l) {
    return (swapLong(longToBytes(l), 0));
  }

  /**
   * Returns the float resulting from reversing 4 bytes of a specified
   * float.
   *
   * @param v input value for which byte reversal is desired
   * @return the value represented by the bytes of v
   *         reversed
   */
  static public float swapFloat(float v) {
    int l = swapInt(Float.floatToIntBits(v));
    return (Float.intBitsToFloat(l));
  }

  /**
   * Returns the double resulting from reversing 8 bytes of a specified
   * double.
   *
   * @param v input value for which byte reversal is desired
   * @return the value represented by the bytes of v
   *         reversed
   */
  static public double swapDouble(double v) {
    long l = swapLong(Double.doubleToLongBits(v));
    return (Double.longBitsToDouble(l));
  }

  /**
   * Convert a short to an array of 2 bytes.
   *
   * @param v input value
   * @return the corresponding array of bytes
   */
  static public byte[] shortToBytes(short v) {
    byte[] b = new byte[2];
    int allbits = 255;
    for (int i = 0; i < 2; i++) {
      b[1 - i] = (byte) ((v & (allbits <> i * 8);
    }
    return b;
  }

  /**
   * Convert an int to an array of 4 bytes.
   *
   * @param v input value
   * @return the corresponding array of bytes
   */
  static public byte[] intToBytes(int v) {
    byte[] b = new byte[4];
    int allbits = 255;
    for (int i = 0; i < 4; i++) {
      b[3 - i] = (byte) ((v & (allbits <> i * 8);
    }
    return b;
  }

  /**
   * Convert a long to an array of 8 bytes.
   *
   * @param v input value
   * @return the corresponding array of bytes
   */
  static public byte[] longToBytes(long v) {
    byte[] b = new byte[8];
    long allbits = 255;
    for (int i = 0; i < 8; i++) {
      b[7 - i] = (byte) ((v & (allbits <> i * 8);
    }
    return b;
  }
}


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

import ucar.ma2.*;
import ucar.nc2.*;
import ucar.nc2.constants.CF;
import ucar.nc2.constants._Coordinate;
import ucar.nc2.constants.AxisType;
import ucar.nc2.dataset.transform.AbstractTransformBuilder;
import ucar.nc2.dataset.transform.VertTransformBuilderIF;
import ucar.nc2.util.CancelTask;
import ucar.nc2.dataset.*;
import ucar.unidata.geoloc.vertical.HybridSigmaPressure;
import ucar.unidata.geoloc.vertical.AtmosSigma;
import ucar.unidata.util.Parameter;

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

/**
 * CSM-1 Convention. Deprecated: use CF
 *
 * @author caron
 */

public class CSMConvention extends COARDSConvention {

  public CSMConvention() {
    this.conventionName = "NCAR-CSM";
  }

  @Override
  public void augmentDataset(NetcdfDataset ds, CancelTask cancelTask) throws IOException {

    List<Variable> vars = ds.getVariables();
    for (Variable var : vars) {
      String unit = var.getUnitsString();
      if (unit != null) {
        if (unit.equalsIgnoreCase("hybrid_sigma_pressure") || unit.equalsIgnoreCase("sigma_level")) {
          // both a coordinate axis and transform
          var.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.GeoZ.toString()));
          var.addAttribute(new Attribute(_Coordinate.TransformType, TransformType.Vertical.toString()));
          var.addAttribute(new Attribute(_Coordinate.Axes, var.getFullName()));
        }
      }
    }

  }

  /**
   * The attribute "coordinates" is an alias for _CoordinateAxes.
   */
  protected void findCoordinateAxes(NetcdfDataset ds) {

    // coordinates is an alias for _CoordinateAxes
    for (VarProcess vp : varList) {
      if (vp.coordAxes == null) { // dont override if already set
        String coordsString = ds.findAttValueIgnoreCase(vp.v, CF.COORDINATES, null);
        if (coordsString != null) {
          vp.coordinates = coordsString;
        }
      }
    }

    super.findCoordinateAxes(ds);
  }

  protected CoordinateTransform makeCoordinateTransform(NetcdfDataset ds, Variable ctv) {
    CoordinateTransform ct = null;

    String unit = ctv.getUnitsString();
    if (unit != null) {
      if (unit.equalsIgnoreCase("hybrid_sigma_pressure")) {
        HybridSigmaPressureBuilder b = new HybridSigmaPressureBuilder();
        ct = b.makeCoordinateTransform(ds, ctv);

      } else if (unit.equalsIgnoreCase("sigma_level")) { // LOOK - no test case for CSM Sigma Vertical coord ??
        SigmaBuilder b = new SigmaBuilder();
        ct = b.makeCoordinateTransform(ds, ctv);
      }
    }
    if (ct != null)
      return ct;

    return super.makeCoordinateTransform(ds, ctv);
  }

  private class HybridSigmaPressureBuilder extends AbstractTransformBuilder implements VertTransformBuilderIF {
    public String getTransformName() {
      return "csm_hybrid_sigma_pressure";
    }

    public VerticalCT makeCoordinateTransform(NetcdfDataset ds, AttributeContainer ctv) {
      VerticalCT rs = new VerticalCT(ctv.getName(), getTransformName(), VerticalCT.Type.HybridSigmaPressure, this);
      rs.addParameter(new Parameter("formula", "pressure(x,y,z) = a(z)*p0 + b(z)*surfacePressure(x,y)"));

      if (!addParameter2(rs, HybridSigmaPressure.PS, ds, ctv, "PS_var", false)) return null;
      if (!addParameter2(rs, HybridSigmaPressure.A, ds, ctv, "A_var", false)) return null;
      if (!addParameter2(rs, HybridSigmaPressure.B, ds, ctv, "B_var", false)) return null;
      if (!addParameter2(rs, HybridSigmaPressure.P0, ds, ctv, "P0_var", false)) return null;
      parseInfo.format("CSMConvention made SigmaPressureCT %s%n",ctv.getName());
      return rs;
    }

    public ucar.unidata.geoloc.vertical.VerticalTransform makeMathTransform(NetcdfDataset ds, Dimension timeDim, VerticalCT vCT) {
      return new HybridSigmaPressure(ds, timeDim, vCT.getParameters());
    }
  }

  private class SigmaBuilder extends AbstractTransformBuilder implements VertTransformBuilderIF {
    public String getTransformName() {
      return "csm_sigma_level";
    }

    public VerticalCT makeCoordinateTransform(NetcdfDataset ds, AttributeContainer ctv) {
      VerticalCT rs = new VerticalCT("sigma-" + ctv.getName(), conventionName, VerticalCT.Type.Sigma, this);
      rs.addParameter(new Parameter("formula", "pressure(x,y,z) = ptop + sigma(z)*(surfacePressure(x,y)-ptop)"));

      if (!addParameter2(rs, AtmosSigma.PS, ds, ctv, "PS_var", false)) return null;
      if (!addParameter2(rs, AtmosSigma.SIGMA, ds, ctv, "B_var", false)) return null;
      if (!addParameter2(rs, AtmosSigma.PTOP, ds, ctv, "P0_var", false)) return null;
      parseInfo.format("CSMConvention made SigmaCT %s%n", ctv.getName());
      return rs;
    }

    public ucar.unidata.geoloc.vertical.VerticalTransform makeMathTransform(NetcdfDataset ds, Dimension timeDim, VerticalCT vCT) {
      return new AtmosSigma(ds, timeDim, vCT.getParameters());
    }
  }

  /**
   * Add a Parameter to a CoordinateTransform. The variable attribute points to a another variable that has the data in it.
   * Make sure that atrribute and variable exist. Id readData is true, read the data and use it as the value of the
   * parameter, otherwise use the name as the value of the parameter.
   *
   * @param rs        the CoordinateTransform
   * @param paramName the parameter name
   * @param ds        dataset
   * @param v         variable
   * @param attName   variable attribute name
   * @param readData  if true, read data and use a  s parameter value
   * @return true if success, false is failed
   */
  protected boolean addParameter2(CoordinateTransform rs, String paramName, NetcdfFile ds, AttributeContainer v, String attName, boolean readData) {
    String varName;
    if (null == (varName = v.findAttValueIgnoreCase(attName, null))) {
      parseInfo.format("CSMConvention No Attribute named %s%n", attName);
      return false;
    }
    varName = varName.trim();

    Variable dataVar;
    if (null == (dataVar = ds.findVariable(varName))) {
      parseInfo.format("CSMConvention No Variable named %s%n", varName);
      return false;
    }

    if (readData) {
      Array data;
      try {
        data = dataVar.read();
      } catch (IOException e) {
        parseInfo.format("CSMConvention failed on read of %s err= %s%n", varName, e.getMessage());
        return false;
      }
      double[] vals = (double []) data.get1DJavaArray(DataType.DOUBLE);
      rs.addParameter(new Parameter(paramName, vals));

    } else
      rs.addParameter(new Parameter(paramName, varName));

    return true;
  }

}


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

import ucar.ma2.DataType;
import ucar.nc2.util.Indent;

import java.util.ArrayList;
import java.util.Formatter;
import java.util.List;
import java.util.Collections;

/**
 * A Group is a logical collection of Variables.
 * The Groups in a Dataset form a hierarchical tree, like directories on a disk.
 * A Group has a name and optionally a set of Attributes.
 * There is always at least one Group in a dataset, the root Group, whose name is the empty string.
 * <p> Immutable if setImmutable() was called.
 *
 * @author caron
 */
public class Group extends CDMNode implements AttributeContainer {

  static List<Group> collectPath(Group g) {
    List<Group> list = new ArrayList<>();
    while (g != null) {
      list.add(0, g);
      g = g.getParentGroup();
    }
    return list;
  }

  protected NetcdfFile ncfile;
  protected List<Variable> variables = new ArrayList<>();
  protected List<Dimension> dimensions = new ArrayList<>();
  protected List<Group> groups = new ArrayList<>();
  protected AttributeContainerHelper attributes;
  protected List<EnumTypedef> enumTypedefs = new ArrayList<>();
  private int hashCode = 0;

  /**
   * Get the full name, starting from the root Group.
   *
   * @return group full name
   */
/* see CDMNode.getFullName()
  public String getFullName() {
    String name = getShortName();
    Group parent = getParentGroup();
    if(parent == null) // we are the root group
	return name;
    else if(parent == ncfile.getRootGroup()) // we are just below root group
	return name; // this does not seem right; should it not return /name?
    else
	return parent.getFullName() + "/" + name;
  }
*/

  /**
   * Alias for getFullName
   * Deprecated because it is unclear
   * to the caller if it is short or fullname.
   *
   * @return group full name
   */
/*
   @Deprecated
   public String getName() {
    return getFullName();
   }
*/

  /**
   * Is this the root group?
   *
   * @return true if root group
   */
  public boolean isRoot() {
    return getParentGroup() == null;
  }

  /**
   * Get the "short" name, unique within its parent Group.
   *
   * @return group short name
   */
  public String getShortName() {
    return shortName;
  }

  /**
   * Get the Variables contained directly in this group.
   *
   * @return List of type Variable; may be empty, not null.
   */
  public java.util.List<Variable> getVariables() {
    return variables;
  }

  /**
   * Find the Variable with the specified (short) name in this group.
   *
   * @param varShortName short name of Variable within this group.
   * @return the Variable, or null if not found
   */
  public Variable findVariable(String varShortName) {
    if (varShortName == null) return null;

    for (Variable v : variables) {
      if (varShortName.equals(v.getShortName()))
        return v;
    }
    return null;
  }

  /*
   * Find the Variable with the specified escaped (short) name in this group.
   * @param varShortNameEscaped escaped short name of Variable within this group.
   * @return the Variable, or null if not found
   * @see NetcdfFile#escapeName
   * @see NetcdfFile#unescapeName
   *
  public Variable findVariableEscaped(String varShortNameEscaped) {
    if (varShortNameEscaped == null) return null;
    return findVariable( NetcdfFile.makeNameUnescaped(varShortNameEscaped));
  } */

  /**
   * Find the Variable with the specified (short) name in this group or a parent group.
   *
   * @param varShortName short name of Variable.
   * @return the Variable, or null if not found
   */
  public Variable findVariableOrInParent(String varShortName) {
    if (varShortName == null) return null;

    Variable v = findVariable(varShortName);
    Group parent = getParentGroup();
    if ((v == null) && (parent != null))
      v = parent.findVariableOrInParent(varShortName);
    return v;
  }

  /**
   * Get the Groups contained directly in this Group.
   *
   * @return List of type Group; may be empty, not null.
   */
  public java.util.List<Group> getGroups() {
    return groups;
  }

  /**
   * Get the owning NetcdfFile
   *
   * @return owning NetcdfFile.
   */
  public NetcdfFile getNetcdfFile() {
    return ncfile;
  }

  /**
   * Retrieve the Group with the specified (short) name.
   *
   * @param groupShortName short name of the nested group you are looking for.
   * @return the Group, or null if not found
   */
  public Group findGroup(String groupShortName) {
    if (groupShortName == null) return null;
    // groupShortName = NetcdfFile.makeNameUnescaped(groupShortName);

    for (Group group : groups) {
      if (groupShortName.equals(group.getShortName()))
        return group;
    }

    return null;
  }

  /**
   * Get the shared Dimensions contained directly in this group.
   *
   * @return List of type Dimension; may be empty, not null.
   */
  public java.util.List<Dimension> getDimensions() {
    return dimensions;
  }

  /**
   * Get the enumerations contained directly in this group.
   *
   * @return List of type EnumTypedef; may be empty, not null.
   */
  public java.util.List<EnumTypedef> getEnumTypedefs() {
    return enumTypedefs;
  }

  /**
   * Retrieve a Dimension using its (short) name. If it doesnt exist in this group,
   * recursively look in parent groups.
   *
   * @param name Dimension name.
   * @return the Dimension, or null if not found
   */
  public Dimension findDimension(String name) {
    if (name == null) return null;
    // name = NetcdfFile.makeNameUnescaped(name);
    Dimension d = findDimensionLocal(name);
    if (d != null) return d;
    Group parent = getParentGroup();
    if (parent != null)
      return parent.findDimension(name);

    return null;
  }

  /**
   * Retrieve a Dimension using its (short) name, in this group only
   *
   * @param name Dimension name.
   * @return the Dimension, or null if not found
   */
  public Dimension findDimensionLocal(String name) {
    if (name == null) return null;
    // name =  NetcdfFile.makeNameUnescaped(name);
    for (Dimension d : dimensions) {
      if (name.equals(d.getShortName()))
        return d;
    }

    return null;
  }

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

  public java.util.List<Attribute> getAttributes() {
    return attributes.filter(attributes,Attribute.SPECIALS).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);
  }

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

  /**
   * Find an Enumeration Typedef using its (short) name. If it doesnt exist in this group,
   * recursively look in parent groups.
   *
   * @param name Enumeration name.
   * @return the Enumeration, or null if not found
   */
  public EnumTypedef findEnumeration(String name) {
    if (name == null) return null;
    // name =  NetcdfFile.makeNameUnescaped(name);
    for (EnumTypedef d : enumTypedefs) {
      if (name.equals(d.getShortName()))
        return d;
    }
    Group parent = getParentGroup();
    if (parent != null)
      return parent.findEnumeration(name);

    return null;
  }

  /**
   * Get the common parent of this and the other group.
   * Cant fail, since the root group is always a parent of any 2 groups.
   *
   * @param other the other group
   * @return common parent of this and the other group
   */
  public Group commonParent(Group other) {
    if (isParent(other)) return this;
    if (other.isParent(this)) return other;
    while (!other.isParent(this))
      other = other.getParentGroup();
    return other;
  }

  /**
   * Is this a parent of the other Group?
   *
   * @param other another Group
   * @return true is it is equal or a parent
   */
  public boolean isParent(Group other) {
    while ((other != this) && (other.getParentGroup() != null))
      other = other.getParentGroup();
    return (other == this);
  }

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

  /**
   * Get String with name and attributes. Used in short descriptions like tooltips.
   *
   * @return name and attributes String.
   */
  public String getNameAndAttributes() {
    StringBuilder sbuff = new StringBuilder();
    sbuff.append("Group ");
    sbuff.append(getShortName());
    sbuff.append("\n");
    for (Attribute att : attributes.getAttributes()) {
      sbuff.append("  ").append(getShortName()).append(":");
      sbuff.append(att.toString());
      sbuff.append(";");
      sbuff.append("\n");
    }
    return sbuff.toString();
  }

  /**
   * CDL representation.
   *
   * @param strict if true, write in strict adherence to CDL definition.
   * @return CDL representation.
   */
  public String writeCDL(boolean strict) {
    Formatter buf = new Formatter();
    writeCDL(buf, new Indent(2), strict);
    return buf.toString();
  }

  protected void writeCDL(Formatter out, Indent indent, boolean strict) {
    boolean hasE = (enumTypedefs.size() > 0);
    boolean hasD = (dimensions.size() > 0);
    boolean hasV = (variables.size() > 0);
    // boolean hasG = (groups.size() > 0);
    boolean hasA = (attributes.getAttributes().size() > 0);

    if (hasE) {
      out.format("%stypes:%n", indent);
      indent.incr();
      for (EnumTypedef e : enumTypedefs) {
        e.writeCDL(out, indent, strict);
        out.format("%n");
      }
      indent.decr();
      out.format("%n");
    }

    if (hasD) {
      out.format("%sdimensions:%n", indent);
      indent.incr();
      for (Dimension myd : dimensions) {
        myd.writeCDL(out, indent, strict);
        out.format("%n");
      }
      indent.decr();
    }

    if (hasV) {
      out.format("%svariables:%n", indent);
      indent.incr();
      for (Variable v : variables) {
        v.writeCDL(out, indent, false, strict);
        out.format("%n");
      }
      indent.decr();
    }

    for (Group g : groups) {
      String gname = strict ? NetcdfFile.makeValidCDLName(g.getShortName()) : g.getShortName();
      out.format("%sgroup: %s {%n", indent, gname);
      indent.incr();
      g.writeCDL(out, indent, strict);
      indent.decr();
      out.format("%s}%n%n", indent);
    }

    //if (hasA && (hasE || hasD || hasV || hasG))
    //  out.format("%n");

    if (hasA) {
      if (isRoot())
        out.format("%s// global attributes:%n", indent);
      else
        out.format("%s// group attributes:%n", indent);

      for (Attribute att : attributes.getAttributes()) {
        //String name = strict ? NetcdfFile.escapeNameCDL(getShortName()) : getShortName();
        if(!Attribute.isspecial(att)) {
          out.format("%s", indent);
          att.writeCDL(out, strict, null);
          out.format(";");
          if(!strict && (att.getDataType() != DataType.STRING)) out.format(" // %s", att.getDataType());
          out.format("%n");
        }
      }
    }
  }

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

  /**
   * Constructor
   *
   * @param ncfile    NetcdfFile owns this Group
   * @param parent    parent of Group. If null, this is the root Group.
   * @param shortName short name of Group.
   */
  public Group(NetcdfFile ncfile, Group parent, String shortName) {
    super(shortName);
    this.ncfile = ncfile;
    this.attributes = new AttributeContainerHelper(shortName);
    setParentGroup(parent == null ? ncfile.getRootGroup() : parent);
  }

  /**
   * Set the Group's parent Group
   *
   * @param parent parent group.
   */
  public void setParentGroup(Group parent) {
    if (immutable) throw new IllegalStateException("Cant modify");
    super.setParentGroup(parent == null ? ncfile.getRootGroup() : parent);
  }

  /**
   * 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();
  }

  /**
   * Adds the specified shared dimension to this group.
   *
   * @param dim  the dimension to add.
   * @throws IllegalStateException  if this dimension is {@link #setImmutable() immutable}.
   * @throws IllegalArgumentException  if {@code dim} isn't shared or a dimension with {@code dim}'s name already
   *                                   exists within the group.
   */
  public void addDimension(Dimension dim) {
    if (immutable) throw new IllegalStateException("Cant modify");

    if (!dim.isShared()) {
      throw new IllegalArgumentException("Dimensions added to a group must be shared.");
    }

    if (findDimensionLocal(dim.getShortName()) != null)
      throw new IllegalArgumentException("Dimension name (" + dim.getShortName() + ") must be unique within Group " + getShortName());

    dimensions.add(dim);
    dim.setGroup(this);
  }

  /**
   * Adds the specified shared dimension to this group, but only if another dimension with the same name doesn't
   * already exist.
   *
   * @param dim  the dimension to add.
   * @return {@code true} if {@code dim} was successfully added to the group. Otherwise, {@code false} will be returned,
   *         meaning that a dimension with {@code dim}'s name already exists within the group.
   * @throws IllegalStateException  if this dimension is {@link #setImmutable() immutable}.
   * @throws IllegalArgumentException  if {@code dim} isn't shared.
   */
  public boolean addDimensionIfNotExists(Dimension dim) {
    if (immutable) throw new IllegalStateException("Cant modify");

    if (!dim.isShared()) {
      throw new IllegalArgumentException("Dimensions added to a group must be shared.");
    }

    if (findDimensionLocal(dim.getShortName()) != null)
      return false;

    dimensions.add(dim);
    dim.setGroup(this);
    return true;
  }

  /**
   * Add a nested Group
   *
   * @param g add this Group.
   */
  public void addGroup(Group g) {
    if (immutable) throw new IllegalStateException("Cant modify");

    if (findGroup(g.getShortName()) != null)
      throw new IllegalArgumentException("Group name (" + g.getShortName() + ") must be unique within Group " + getShortName());

    groups.add(g);
    g.setParentGroup(this); // groups are a tree - only one parent
  }

  /**
   * Add an Enumeration
   *
   * @param e add this Enumeration.
   */
  public void addEnumeration(EnumTypedef e) {
    if (immutable) throw new IllegalStateException("Cant modify");
    if (e == null) return;
    e.setParentGroup(this);
    enumTypedefs.add(e);
  }

  /**
   * Add a Variable
   *
   * @param v add this Variable.
   */
  public void addVariable(Variable v) {
    if (immutable) throw new IllegalStateException("Cant modify");
    if (v == null) return;

    if (findVariable(v.getShortName()) != null) {
      //Variable other = findVariable(v.getShortName()); // debug
      throw new IllegalArgumentException("Variable name (" + v.getShortName() + ") must be unique within Group " + getShortName());
    }

    variables.add(v);
    v.setParentGroup(this); // variable can only be in one group
  }

  /**
   * Remove an Dimension : uses the dimension hashCode to find it.
   *
   * @param d remove this Dimension.
   * @return true if was found and removed
   */
  public boolean remove(Dimension d) {
    if (immutable) throw new IllegalStateException("Cant modify");
    return d != null && dimensions.remove(d);
  }

  /**
   * Remove an Attribute : uses the Group hashCode to find it.
   *
   * @param g remove this Group.
   * @return true if was found and removed
   */
  public boolean remove(Group g) {
    if (immutable) throw new IllegalStateException("Cant modify");
    return g != null && groups.remove(g);
  }

  /**
   * Remove a Variable : uses the variable hashCode to find it.
   *
   * @param v remove this Variable.
   * @return true if was found and removed
   */
  public boolean remove(Variable v) {
    if (immutable) throw new IllegalStateException("Cant modify");
    return v != null && variables.remove(v);
  }

  /**
   * remove a Dimension using its name, in this group only
   *
   * @param dimName Dimension name.
   * @return true if dimension found and removed
   */
  public boolean removeDimension(String dimName) {
    if (immutable) throw new IllegalStateException("Cant modify");
    for (int i = 0; i < dimensions.size(); i++) {
      Dimension d = dimensions.get(i);
      if (dimName.equals(d.getShortName())) {
        dimensions.remove(d);
        return true;
      }
    }
    return false;
  }

  /**
   * remove a Variable using its (short) name, in this group only
   *
   * @param shortName Variable name.
   * @return true if Variable found and removed
   */
  public boolean removeVariable(String shortName) {
    if (immutable) throw new IllegalStateException("Cant modify");
    for (int i = 0; i < variables.size(); i++) {
      Variable v = variables.get(i);
      if (shortName.equals(v.getShortName())) {
        variables.remove(v);
        return true;
      }
    }
    return false;
  }

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

  @Override
  public String toString() {
    return writeCDL(false);
  }

  /**
   * Instances which have same name and parent are equal.
   */
  @Override
  public boolean equals(Object oo) {
    if (this == oo) return true;
    if (!(oo instanceof Group)) return false;
    Group og = (Group) oo;
    if (!getShortName().equals(og.getShortName()))
      return false;
    return !((getParentGroup() != null) && !getParentGroup().equals(og.getParentGroup()));
  }

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

  public void hashCodeShow(Indent indent) {
    System.out.printf("%sGroup hash = %d%n", indent, hashCode());
    System.out.printf("%s shortName %s = %d%n", indent, getShortName(), getShortName().hashCode());
    System.out.printf("%s parentGroup %s = %d%n", indent, getParentGroup(), getParentGroup().hashCode());
  }

  /**
   * Create groups to ensure path is defined
   *
   * @param ncf        the containing netcdf file object
   * @param path       the path to the desired group
   * @param ignorelast true => ignore last element in the path
   * @return the Group, or null if not found
   */
  public Group makeRelativeGroup(NetcdfFile ncf, String path, boolean ignorelast) {
    path = path.trim();
    path = path.replace("//", "/");
    boolean isabsolute = (path.charAt(0) == '/');
    if (isabsolute)
      path = path.substring(1);

    // iteratively create path
    String pieces[] = path.split("/");
    if (ignorelast) pieces[pieces.length - 1] = null;

    Group current = (isabsolute ? ncfile.getRootGroup() : this);
    for (String name : pieces) {
      if (name == null) continue;
      String clearname = NetcdfFile.makeNameUnescaped(name);  //??
      Group next = current.findGroup(clearname);
      if (next == null) {
        next = new Group(ncf, current, clearname);
        current.addGroup(next);
      }
      current = next;
    }
    return current;
  }
}