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

package ucar.nc2.grib;

import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import ucar.nc2.Attribute;
import ucar.nc2.constants.CDM;
import ucar.nc2.dt.GridDataset;
import ucar.nc2.dt.GridDatatype;
import ucar.unidata.util.StringUtil2;

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

/**
 * @deprecated  As of version 4.5.0 - this was only intended to be
 * used for the 4.2 - 4.3 transition. Extended to work with 4.5 dataset
 * names, but deprecated to reminder users to update the variable names
 * used in their applications.
 *
 * Try to figure out what GRIB name in 4.2 maps to in 4.3 NCEP IDD datasets.
 * Not guaranteed to be correct.
 *
 * See ToolsUI IOSP/GRIB2/GRIB-RENAME
 *
 * @author caron
 * @since 4/7/12
 */
@Deprecated
public class GribVariableRenamer {
  private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(GribVariableRenamer.class);

  private static HashMap<String, Renamer> map1;
  private static HashMap<String, Renamer> map2;

  ////////////////////////////////////////////////                        `
  //Map<String, List<String>> mapr = new HashMap<String, List<String>>();

  private void initMap1() {
    List<VariableRenamerBean> beans = readVariableRenameFile("resources/grib1/grib1VarMap.xml");
    map1 = makeMapBeans(beans);
  }

  private void initMap2() {
    List<VariableRenamerBean> beans = readVariableRenameFile("resources/grib2/grib2VarMap.xml");
    map2 = makeMapBeans(beans);
  }

  List<String> getMappedNamesGrib2(String oldName) {
    if (map2 == null) initMap2();
    List<String> result = new ArrayList<>();
    Renamer mbean = map2.get(oldName);
    if (mbean == null) return null;
    for (VariableRenamerBean r : mbean.newVars) {
      result.add(r.newName);
    }
    return result;
  }

  List<String> getMappedNamesGrib1(String oldName) {
    if (map1 == null) initMap1();
    List<String> result = new ArrayList<>();
    Renamer mbean = map1.get(oldName);
    if (mbean == null) return null;
    for (VariableRenamerBean r : mbean.newVars) {
      result.add(r.newName);
    }
    return result;
  }

  /**
   * Look for possible matches of old (4.2) grib names in new (4.3) dataset.
   *
   * @param gds check existence in this dataset. Must be from a GRIB1 or GRIB2 dataset.
   * @param oldName old name from 4.2 dataset
   * @return list of possible matches (as grid short name), each exists in the dataset
   */
  List<String> matchNcepNames(GridDataset gds, String oldName) {
    List<String> result = new ArrayList<>();

    // look for exact match
    if (contains(gds, oldName))  {
      result.add(oldName);
      return result;
    }

    Attribute att = gds.findGlobalAttributeIgnoreCase(CDM.FILE_FORMAT);
    boolean isGrib1 = (att != null) && att.getStringValue().startsWith("GRIB-1");
    boolean isGrib2 = (att != null) && att.getStringValue().startsWith("GRIB-2");
    HashMap<String, Renamer> map;
    if (isGrib1) {
      if (map1 == null) initMap1();
      map = map1;

    } else if (isGrib2) {
      if (map2 == null) initMap2();
      map = map2;

    } else {
      return result; // empty list
    }

    // look in our renamer map
    Renamer mbean = map.get(oldName);
    if (mbean != null && mbean.newName != null && contains(gds, mbean.newName))  {
      result.add(mbean.newName); // if its unique, then we are done
      return result;
    }

    // not unique - match against NCEP dataset
    if (mbean != null) {
      String dataset = extractDatasetFromLocation(gds.getLocation());
      for (VariableRenamerBean r : mbean.newVars) {
        if (r.getDatasetType().equals(dataset) && contains(gds, r.newName)) result.add(r.newName);
      }
      if (result.size() == 1) return result; // return if unique
    }

    // not unique, no unique match against dataset - check existence in the dataset
    result.clear();
    if (mbean != null) {
      for (VariableRenamerBean r : mbean.newVarsMap.values()) {
        if (contains(gds, r.newName)) result.add(r.newName);
      }
      if (result.size() > 0) return result;
    }

    // try to map oldName -> new prefix
    result.clear();
    String oldMunged = munge(oldName);
    for (GridDatatype grid : gds.getGrids()) {
      String newMunged = munge(grid.getShortName());
      if (newMunged.startsWith(oldMunged))
        result.add(grid.getShortName());
    }
    if (result.size() > 0) return result;

    // return empty list
    return result;
  }

  List<String> matchNcepNames(String datasetType, String oldName) {
    boolean isGrib1 = datasetType.endsWith("grib1");
    List<String> result = new ArrayList<>();

    HashMap<String, Renamer> map;
    if (isGrib1) {
      if (map1 == null) initMap1();
      map = map1;

    } else {
      if (map2 == null) initMap2();
      map = map2;
    }

    // look in our renamer map
    Renamer mbean = map.get(oldName);
    if (mbean != null && mbean.newName != null)  {
      result.add(mbean.newName); // if its unique, then we are done
      return result;
    }

    // not unique - match against NCEP datasetType
    if (mbean != null) {
      for (VariableRenamerBean r : mbean.newVars) {
        if (r.getDatasetType().equals(datasetType)) result.add(r.newName);
      }
    }

    return result;
  }

  private String munge(String old) {
    StringBuilder oldLower = new StringBuilder( old.toLowerCase());
    StringUtil2.remove(oldLower, "_-");
    return oldLower.toString();
  }

  private boolean contains(GridDataset gds, String name) {
    return gds.findGridByShortName(name) != null;
  }

  private String getNewName(HashMap<String, Renamer> map, String datasetLocation, String oldName) {
    Renamer mbean = map.get(oldName);
    if (mbean == null) return null; // ??
    if (mbean.newName != null) return mbean.newName; // if its unique, then we are done
    String dataset = extractDatasetFromLocation(datasetLocation);
    for (VariableRenamerBean r : mbean.newVars) {
      if (r.getDatasetType().equals(dataset)) return r.getNewName();
    }
    return null; // ??
  }

  private static String extractDatasetFromLocation(String location) {
    // check if location is from 4.5 server, and if so, remove the trailing /GC
    // This is only a temporary fix, as this renaming was only supposed to be applied
    // for the 4.2 to 4.3 transition. Thus, I plan on marking this class as deprecated
    // in the 4.5 branch
    if (location.endsWith("/GC")) {
      int locLen = location.length();
      location = location.substring(0, locLen - 3);
    }
    int pos = location.lastIndexOf("/");
    if (pos > 0) location = location.substring(pos+1);
    int posSuffix = location.lastIndexOf(".");
    if (posSuffix-14 > 0)
      return location.substring(0, posSuffix-14) + location.substring(posSuffix);
    return "";
  }

    /*
  <dataset name="DGEX_Alaska_12km_20100524_0000.grib2">
    <param oldName="Geopotential_height" newName="Geopotential_height_pressure" varId="VAR_0-3-5_L100" />
    <param oldName="Geopotential_height_surface" newName="Geopotential_height_surface" varId="VAR_0-3-5_L1" />
    <param oldName="MSLP_Eta_Reduction" newName="MSLP_Eta_model_reduction_msl" varId="VAR_0-3-192_L101" />
    <param oldName="Maximum_temperature" newName="Maximum_temperature_height_above_ground" varId="VAR_0-0-4_L103" />
    <param oldName="Minimum_temperature" newName="Minimum_temperature_height_above_ground" varId="VAR_0-0-5_L103" />
  </dataset>
  */

  // debugging only
  List<VariableRenamerBean> readVariableRenamerBeans(String which) {
    if (which.equals("GRIB-1"))
      return readVariableRenameFile("resources/grib1/grib1VarMap.xml");
    else
      return readVariableRenameFile("resources/grib2/grib2VarMap.xml");
  }

  private List<VariableRenamerBean> readVariableRenameFile(String path) {
    java.util.List<VariableRenamerBean> beans = new ArrayList<>(1000);
    try (InputStream is = GribResourceReader.getInputStream(path)) {
      if (is == null) {
        logger.warn("Cant read file " + path);
        return null;
      }

      SAXBuilder builder = new SAXBuilder();
      org.jdom2.Document doc = builder.build(is);
      Element root = doc.getRootElement();
      List<Element> dsElems = root.getChildren("dataset");
      for (Element dsElem : dsElems) {
        String dsName = dsElem.getAttributeValue("name");
        List<Element> params = dsElem.getChildren("param");
        for (Element elem : params) {
          String oldName = elem.getAttributeValue("oldName");
          String newName = elem.getAttributeValue("newName");
          String varId = elem.getAttributeValue("varId");
          beans.add(new VariableRenamerBean(dsName, oldName, newName, varId));
        }
      }
      return beans;

    } catch (IOException | JDOMException ioe) {
      ioe.printStackTrace();
      return null;
    }
  }

  @Deprecated
  public static class VariableRenamerBean implements Comparable<VariableRenamerBean> {
    String dsName, dsType, oldName, newName, varId;

    // no-arg constructor
    public VariableRenamerBean() {
    }

    public VariableRenamerBean(String dsName, String oldName, String newName, String varId) {
      this.dsName = dsName;
      this.dsType = extractDatasetFromLocation(dsName);
      this.oldName = oldName;
      this.newName = newName;
      this.varId = varId;
    }

    public String getDataset() {
      return dsName;
    }

    public String getDatasetType() {
      return dsType;
    }

    public String getVarId() {
      return varId;
    }

    public String getOldName() {
      return oldName;
    }

    public String getNewName() {
      return newName;
    }

    public String getStatus() {
      if (oldName.equals(newName)) return "*";
      if (oldName.equalsIgnoreCase(newName)) return "**";
      return "";
    }

    @Override
    public int compareTo(VariableRenamerBean o) {
      return newName.compareTo(o.getNewName());
    }
  }

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

  private HashMap<String, Renamer> makeMapBeans(List<VariableRenamerBean> vbeans) {
    HashMap<String, Renamer> map = new HashMap<>(3000);
    for (VariableRenamerBean vbean : vbeans) {

      // construct the old -> new mapping
      Renamer mbean = map.get(vbean.getOldName());
      if (mbean == null) {
        mbean = new Renamer(vbean.getOldName());
        map.put(vbean.getOldName(), mbean);
      }
      mbean.add(vbean);
    }

    for (Renamer rmap : map.values()) {
      rmap.finish();
    }

    return map;
  }
  @Deprecated
  private static class Renamer {
    String oldName, newName; // newName exists when theres only one
    List<VariableRenamerBean> newVars = new ArrayList<>();
    HashMap<String, VariableRenamerBean> newVarsMap = new HashMap<>();

    // no-arg constructor
    public Renamer() {
    }

    public Renamer(String oldName) {
      this.oldName = oldName;
    }

    void add(VariableRenamerBean vbean) {
      newVarsMap.put(vbean.getNewName(), vbean);
      newVars.add(vbean);
    }

    void finish() {
      if (newVarsMap.values().size() == 1) {
        newName = newVars.get(0).getNewName();
      }
    }

    public int getCount() {
      return newVars.size();
    }

    public String getOldName() {
      return oldName;
    }

  }

  public static void main(String[] args) throws IOException {
    ucar.nc2.dt.grid.GridDataset gds = ucar.nc2.dt.grid.GridDataset.open("Q:/cdmUnitTest/tds/ncep/GFS_CONUS_80km_20100513_0600.grib1");

    GribVariableRenamer r = new GribVariableRenamer();
    List<String> result = r.matchNcepNames(gds, "Precipitable_water");
  }

}


package ucar.nc2.ogc.waterml;

import net.opengis.waterml.x20.MeasurementTimeseriesType;
import ucar.nc2.VariableSimpleIF;
import ucar.nc2.ft.PointFeature;
import ucar.nc2.ft.StationTimeSeriesFeature;
import ucar.nc2.ogc.MarshallingUtil;

import java.io.IOException;

/**
 * Created by cwardgar on 2014/03/05.
 */
public abstract class NcMeasurementTimeseriesType {
    // wml2:Collection/wml2:observationMember/om:OM_Observation/om:result/wml2:MeasurementTimeseries
    public static MeasurementTimeseriesType initMeasurementTimeseries(MeasurementTimeseriesType measurementTimeseries,
            StationTimeSeriesFeature stationFeat, VariableSimpleIF dataVar) throws IOException {
        // @gml:id
        String id = MarshallingUtil.createIdForType(MeasurementTimeseriesType.class);
        measurementTimeseries.setId(id);

        // wml2:defaultPointMetadata
        NcTVPDefaultMetadataPropertyType.initDefaultPointMetadata(
                measurementTimeseries.addNewDefaultPointMetadata(), dataVar);

        // wml2:point[0..*]
        for (PointFeature pf : stationFeat) {
            // wml2:point
            Point.initPoint(measurementTimeseries.addNewPoint(), pf, dataVar);
        }

        return measurementTimeseries;
    }

    public abstract static class Point {
        // wml2:Collection/wml2:observationMember/om:OM_Observation/om:result/wml2:MeasurementTimeseries/wml2:point
        public static MeasurementTimeseriesType.Point initPoint(MeasurementTimeseriesType.Point point,
                PointFeature pointFeat, VariableSimpleIF dataVar) throws IOException {
            // wml2:MeasurementTVP
            NcMeasureTVPType.initMeasurementTVP(point.addNewMeasurementTVP(), pointFeat, dataVar);

            return point;
        }

        private Point() { }
    }

    private NcMeasurementTimeseriesType() { }
}


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

import javax.annotation.concurrent.Immutable;

/**
 * Provides support for units that are offset from reference units (ex: as the
 * unit "degree Celsius" is offset from the reference unit "kelvin"). Instances
 * of this class are immutable.
 *
 * @author Steven R. Emmerson
 */
@Immutable
public final class OffsetUnit extends UnitImpl implements DerivableUnit {
    private static final long serialVersionUID = 1L;

    /**
     * The origin of this unit in terms of the reference unit.
     *
     * @serial
     */
    private final double      _offset;

    /**
     * The reference unit.
     *
     * @serial
     */
    private final Unit        _unit;

    /**
     * The derived unit that is convertible with this unit.
     *
     * @serial
     */
    private final DerivedUnit       _derivedUnit;

    /**
     * Constructs from a reference unit and an offset.
     *
     * @param unit
     *            The reference unit.
     * @param offset
     *            The origin of this unit in terms of the reference unit. For
     *            example, a degree Celsius unit would be created as "new
     * 				OffsetUnit(kelvin, 273.15).
     */
    public OffsetUnit(final Unit unit, final double offset) {
        this(unit, offset, null);
    }

    /**
     * Constructs from a reference unit, and offset, and a unit identifier.
     *
     * @param unit
     *            The reference unit.
     * @param offset
     *            The origin of this unit in terms of the reference unit. For
     *            example, a degree Celsius unit would be created as "new
     * 				OffsetUnit(kelvin, 273.15).
     * @param id
     *            The identifier for the new unit.
     */
    public OffsetUnit(final Unit unit, final double offset, final UnitName id) {
        super(id);
        if (!(unit instanceof OffsetUnit)) {
            _unit = unit;
            _offset = offset;
        }
        else {
            _unit = ((OffsetUnit) unit)._unit;
            _offset = ((OffsetUnit) unit)._offset + offset;
        }
      _derivedUnit = _unit.getDerivedUnit();
    }

    static Unit getInstance(final Unit unit, final double origin) {
        return (origin == 0)
                ? unit
                : new OffsetUnit(unit, origin);
    }

    /**
     * Returns the reference unit.
     *
     * @return The reference unit.
     */
    public Unit getUnit() {
        return _unit;
    }

    /**
     * Returns the offset. The offset is the location of the origin of this unit
     * in terms of the reference unit.
     *
     * @return The origin of this unit in terms of the reference unit.
     */
    public double getOffset() {
        return _offset;
    }

    /*
     * From UnitImpl:
     */

    /**
     * Clones this unit, changing the identifier.
     *
     * @param id
     *            The identifier for the new unit.
     * @return This unit with its identifier changed.
     */
    public Unit clone(final UnitName id) {
        return new OffsetUnit(getUnit(), getOffset(), id);
    }

    @Override
    public Unit multiplyBy(final double scale) throws MultiplyException {
        if (scale == 0) {
            throw new MultiplyException(scale, this);
        }
        return getInstance(_unit.multiplyBy(scale), _offset / scale);
    }

    @Override
    public Unit shiftTo(final double origin) {
        return getInstance(_unit, origin + _offset);
    }

    /**
     * Multiply this unit by another unit.
     *
     * @param that
     *            The unit to multiply this unit by.
     * @return The product of this unit and that. The offset of
     *         this unit will be ignored; thus, for example
     *         "celsius.myMultiplyBy(day)" is equivalent to
     *         "kelvin.myMultiplyBy(day)".
     * @throws MultiplyException
     *             Can't multiply these units together.
     */
    @Override
    protected Unit myMultiplyBy(final Unit that) throws MultiplyException {
        return that instanceof OffsetUnit
                ? getUnit().multiplyBy(((OffsetUnit) that).getUnit())
                : getUnit().multiplyBy(that);
    }

    /**
     * Divide this unit by another unit.
     *
     * @param that
     *            The unit to divide this unit by.
     * @return The quotient of this unit and that. The offset of
     *         this unit will be ignored; thus, for example
     *         "celsius.myDivideBy(day)" is equivalent to
     *         "kelvin.myDivideBy(day)".
     * @throws OperationException
     *             Can't divide these units.
     */
    @Override
    protected Unit myDivideBy(final Unit that) throws OperationException {
        return that instanceof OffsetUnit
                ? getUnit().divideBy(((OffsetUnit) that).getUnit())
                : getUnit().divideBy(that);
    }

    /**
     * Divide this unit into another unit.
     *
     * @param that
     *            The unit to divide this unit into.
     * @return The quotient of that unit and this unit. The offset
     *         of this unit will be ignored; thus, for example
     *         "celsius.myDivideInto(day)" is equivalent to
     *         "kelvin.myDivideInto(day)".
     * @throws OperationException
     *             Can't divide these units.
     */
    @Override
    protected Unit myDivideInto(final Unit that) throws OperationException {
        return that instanceof OffsetUnit
                ? getUnit().divideInto(((OffsetUnit) that).getUnit())
                : getUnit().divideInto(that);
    }

    /**
     * Raise this unit to a power.
     *
     * @param power
     *            The power to raise this unit by.
     * @return The result of raising this unit by the power power.
     *         The offset of this unit will be ignored; thus, for example
     *         "celsius.myRaiseTo(2)" is equivalent to "kelvin.myRaiseTo(2)".
     * @throws RaiseException
     *             Can't raise this unit to a power.
     */
    // Ignore offset (e.g. "Cel2" == "K2")
    @Override
    protected Unit myRaiseTo(final int power) throws RaiseException {
        return getUnit().raiseTo(power);
    }

    /**
     * Returns the derived unit that is convertible with this unit.
     *
     * @return The derived unit that is convertible with this unit.
     */
    public DerivedUnit getDerivedUnit() {
        return _derivedUnit;
    }

    /**
     * Converts a value in this unit to the equivalent value in the convertible
     * derived unit.
     *
     * @param amount
     *            The value in this unit.
     * @return The equivalent value in the convertible derived unit.
     * @throws ConversionException
     *             Can't convert between units.
     */
    public float toDerivedUnit(final float amount) throws ConversionException {
        return (float) toDerivedUnit((double) amount);
    }

    /**
     * Converts a value in this unit to the equivalent value in the convertible
     * derived unit.
     *
     * @param amount
     *            The value in this unit.
     * @return The equivalent value in the convertible derived unit.
     * @throws ConversionException
     *             Can't convert between units.
     */
    public double toDerivedUnit(final double amount) throws ConversionException {
        if (!(_unit instanceof DerivableUnit)) {
            throw new ConversionException(this, getDerivedUnit());
        }
        return ((DerivableUnit) getUnit()).toDerivedUnit(amount + getOffset());
    }

    /**
     * Converts values in this unit to the equivalent values in the convertible
     * derived unit.
     *
     * @param input
     *            The values in this unit.
     * @param output
     *            The equivalent values in the convertible derived unit. May be
     *            the same array as input.
     * @return output.
     * @throws ConversionException
     *             Can't convert between units.
     */
    public float[] toDerivedUnit(final float[] input, final float[] output)
            throws ConversionException {
        if (!(_unit instanceof DerivableUnit)) {
            throw new ConversionException(this, getDerivedUnit());
        }
        final float origin = (float) getOffset();
        for (int i = input.length; --i >= 0;) {
            output[i] = input[i] + origin;
        }
        return ((DerivableUnit) getUnit()).toDerivedUnit(output, output);
    }

    /**
     * Converts values in this unit to the equivalent values in the convertible
     * derived unit.
     *
     * @param input
     *            The values in this unit.
     * @param output
     *            The equivalent values in the convertible derived unit. May be
     *            the same array as input.
     * @return output.
     * @throws ConversionException
     *             Can't convert between units.
     */
    public double[] toDerivedUnit(final double[] input, final double[] output)
            throws ConversionException {
        if (!(_unit instanceof DerivableUnit)) {
            throw new ConversionException(this, getDerivedUnit());
        }
        final double origin = getOffset();
        for (int i = input.length; --i >= 0;) {
            output[i] = input[i] + origin;
        }
        return ((DerivableUnit) getUnit()).toDerivedUnit(output, output);
    }

    /**
     * Converts a value in the convertible derived unit to the equivalent value
     * in this unit.
     *
     * @param amount
     *            The value in the convertible derived unit.
     * @return The equivalent value in this unit.
     * @throws ConversionException
     *             Can't convert between units.
     */
    public float fromDerivedUnit(final float amount) throws ConversionException {
        return (float) fromDerivedUnit((double) amount);
    }

    /**
     * Converts a value in the convertible derived unit to the equivalent value
     * in this unit.
     *
     * @param amount
     *            The value in the convertible derived unit.
     * @return The equivalent value in this unit.
     * @throws ConversionException
     *             Can't convert between units.
     */
    public double fromDerivedUnit(final double amount)
            throws ConversionException {
        if (!(_unit instanceof DerivableUnit)) {
            throw new ConversionException(getDerivedUnit(), this);
        }
        return ((DerivableUnit) getUnit()).fromDerivedUnit(amount)
                - getOffset();
    }

    /**
     * Converts values in the convertible derived unit to the equivalent values
     * in this unit.
     *
     * @param input
     *            The values in the convertible derived unit.
     * @param output
     *            The equivalent values in this unit. May be the same array as
     *            input.
     * @return output.
     * @throws ConversionException
     *             Can't convert between units.
     */
    public float[] fromDerivedUnit(final float[] input, final float[] output)
            throws ConversionException {
        if (!(_unit instanceof DerivableUnit)) {
            throw new ConversionException(getDerivedUnit(), this);
        }
        ((DerivableUnit) getUnit()).fromDerivedUnit(input, output);
        final float origin = (float) getOffset();
        for (int i = input.length; --i >= 0;) {
            output[i] -= origin;
        }
        return output;
    }

    /**
     * Converts values in the convertible derived unit to the equivalent values
     * in this unit.
     *
     * @param input
     *            The values in the convertible derived unit.
     * @param output
     *            The equivalent values in this unit. May be the same array as
     *            input.
     * @return output.
     * @throws ConversionException
     *             Can't convert between units.
     */
    public double[] fromDerivedUnit(final double[] input, final double[] output)
            throws ConversionException {
        if (!(_unit instanceof DerivableUnit)) {
            throw new ConversionException(getDerivedUnit(), this);
        }
        ((DerivableUnit) getUnit()).fromDerivedUnit(input, output);
        final double origin = getOffset();
        for (int i = input.length; --i >= 0;) {
            output[i] -= origin;
        }
        return output;
    }

    /**
     * Indicates if this unit is semantically identical to an object.
     *
     * @param object
     *            The object.
     * @return true if and only if this unit is semantically
     *         identical to object
     *				.
     */
    @Override
    public boolean equals(final Object object) {
        if (this == object) {
            return true;
        }
        if (_offset == 0) {
            return object.equals(_unit);
        }
        if (!(object instanceof OffsetUnit)) {
            return false;
        }

        OffsetUnit that = (OffsetUnit) object;
        if (Double.compare(that._offset, _offset) != 0) return false;
        return _unit.equals(that._unit);
    }

    /**
     * Returns the hash code of this instance.
     *
     * @return The hash code of this instance.
     */
    @Override
    public int hashCode() {
        return (getOffset() == 0
                ? 0
                : Double.valueOf(getOffset()).hashCode())
                ^ getUnit().hashCode();
    }

    /**
     * Indicates if this unit is dimensionless.
     *
     * @return true if and only if this unit is dimensionless.
     */
    public boolean isDimensionless() {
        return getUnit().isDimensionless();
    }

    /**
     * Returns the string representation of this unit.
     *
     * @return The string representation of this unit.
     */
    @Override
    public String toString() {
        final String string = super.toString(); // get symbol or name
        return string != null
                ? string
                : getCanonicalString();
    }

    /**
     * Returns the canonical string representation of the unit.
     *
     * @return The canonical string representation.
     */
    public String getCanonicalString() {
        return "(" + getUnit().toString() + ") @ " + getOffset();
    }
}


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

import opendap.dap.*;

import java.io.IOException;
import java.util.Enumeration;
import java.util.Vector;
import java.util.List;
import java.util.ArrayList;

import ucar.ma2.*;
import ucar.nc2.Variable;

/**
 * Convert Dods object tree to netcdf.
 *
 * @author caron
 */
public class ConvertD2N {
  static private org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(DODSNetcdfFile.class);

  /* Difficult cases.
    - V is a top variable vs member of a structure (or group ??)
    - Dgrid.member request may or may not get returned inside a Structure.
    - Structures may have arbitrary nesting.
  */

  /*
   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:
    o scalar primitive (byte, int, float, String, etc): getValue()
    o array of primitive: DArray with PrimitiveVector (BytePrimitiveVector, FloatPrimitiveVector, etc): getValue(i)
      Get internal java array with getInternalStorage().
    o array of String: DArray with BaseTypePrimitiveVector containing String objects
    o scalar DStructure: same as DDS
    o scalar DGrid: same as DDS
    o array of DStructure: DArray with BaseTypePrimitiveVector, whose values are DStructure
    X array of DGrid, DSequence: (not allowd)
    o DSequence: values = Vector (rows) containing Vector (fields)
*/

  /**
   * Convert a DataDDS into an Array for a Structure member variable.
   *
   * @param v must be a member of a structure
   * @param section the requested variable section, as a List of type Range
   * @param dataV the dataDDS has been parsed into this dodsV, this is the top variable containing v
   * @param flatten if true, remove the StructureData "wrapper".
   * @return the data as as Array
   * @throws IOException on io error
   * @throws DAP2Exception on bad things happening
   */
  public Array convertNestedVariable(ucar.nc2.Variable v, List<Range> section, DodsV dataV, boolean flatten) throws IOException, DAP2Exception {
    Array data = convertTopVariable(v, section, dataV);
    if (flatten) {
      ArrayStructure as = (ArrayStructure) data;

      // make list of names
      List<String> names = new ArrayList<>();
      Variable nested = v;
      while (nested.isMemberOfStructure()) {
        names.add( 0, nested.getShortName());
        nested = nested.getParentStructure();
      }

      StructureMembers.Member m = findNested(as, names, v.getShortName());
      Array mdata = m.getDataArray();
      if (mdata instanceof ArraySequenceNested) {
        // gotta unroll
        ArraySequenceNested arraySeq = (ArraySequenceNested) mdata;
        return arraySeq.flatten();
      }
      return mdata;
    }
    return data;
  }

  private StructureMembers.Member findNested(ArrayStructure as, List<String> names, String want) {
    String name = names.get(0);
    StructureMembers sm = as.getStructureMembers();
    StructureMembers.Member m = sm.findMember(name);
    if (name.equals(want))
      return m;

    // keep going
    ArrayStructure nested = (ArrayStructure) m.getDataArray();
    names.remove(0);
    return findNested( nested, names, want);
  }

  /**
   * Convert a DataDDS into an Array for a top level variable, ie not a Structure member variable.
   *
   * @param v must be a top variable
   * @param section the requested variable section
   * @param dataV the dataDDS has been parsed into this dodsV
   * @return the data as as Array
   * @throws IOException on io error
   * @throws DAP2Exception on bad
   */
  public Array convertTopVariable(ucar.nc2.Variable v, List<Range> section, DodsV dataV) throws IOException, DAP2Exception {
    Array data = convert(dataV);

    // arrays
    if ((dataV.darray != null) && (dataV.bt instanceof DString)) {

      if (v.getDataType() == DataType.STRING)
        return convertStringArray(data, v);
      else if (v.getDataType() == DataType.CHAR)
        return convertStringArrayToChar(dataV.darray, v, section);
      else {
        String mess = "DODSVariable convertArray String invalid dataType= "+v.getDataType();
        logger.error(mess);
        throw new IllegalArgumentException( mess);
      }

    }

    if ((dataV.bt instanceof DString) && (v.getDataType() == DataType.CHAR)) {
        // special case: convert String back to CHAR
        return convertStringToChar(data, v);
    }

    return data;

    /* else { // the DGrid case comes here also
         // create the array, using  DODS internal array so there's no copying
        dods.dap.PrimitiveVector pv = dataV.darray.getPrimitiveVector();
        Object storage = pv.getInternalStorage();
        //storage = widenArray( pv, storage); // LOOK data conversion if needed
        int[] shape = (section == null) ? v.getShape() : Range.getShape(section);
        return Array.factory( v.getDataType().getPrimitiveClassType(), shape, storage);
      }   */
  }

  /**
   * Convert a DataDDS into an Array
   *
   * @param dataV the dataDDS has been parsed into this dodsV
   * @return the data as as Array
   * @throws IOException on io error
   * @throws DAP2Exception on bad
   */
  public Array convert(DodsV dataV) throws IOException, DAP2Exception {

    // scalars
    if (dataV.darray == null) {

      if (dataV.bt instanceof DStructure) {
        ArrayStructure structArray = makeArrayStructure( dataV);
        iconvertDataStructure( (DStructure) dataV.bt, structArray.getStructureMembers());
        return structArray;

      } else if (dataV.bt instanceof DGrid) {
        throw new IllegalStateException( "DGrid without a darray");

      } else if (dataV.bt instanceof DSequence) {
        ArrayStructure structArray = makeArrayStructure( dataV);
        iconvertDataSequenceArray( (DSequence) dataV.bt, structArray.getStructureMembers());
        return structArray;

      } else { // scalar
        DataType dtype = dataV.getDataType();
        Array scalarData = Array.factory(  dtype, new int[0]);
        IndexIterator scalarIndex = scalarData.getIndexIterator();
        iconvertDataPrimitiveScalar( dataV.bt, scalarIndex);
        return scalarData;
      }
    }

    // arrays
    if (dataV.darray != null) {

      if (dataV.bt instanceof DStructure) {
        ArrayStructure structArray = makeArrayStructure( dataV);
        iconvertDataStructureArray( dataV.darray, structArray.getStructureMembers());
        return structArray;

      } else if (dataV.bt instanceof DString) {
        return convertStringArray(dataV.darray);

      } else {
        // the DGrid case comes here also
        // create the array, using  DODS internal array so there's no copying
        opendap.dap.PrimitiveVector pv = dataV.darray.getPrimitiveVector();
        Object storage = pv.getInternalStorage();
        DataType dtype = dataV.getDataType();
        return Array.factory( dtype, makeShape( dataV.darray), storage);
      }
    }

    String mess = "Unknown baseType "+dataV.bt.getClass().getName()+" name="+ dataV.getEncodedName();
    logger.error(mess);
    throw new IllegalStateException(mess);
  }

  private ArrayStructure makeArrayStructure(DodsV dataV) {
    StructureMembers members = new StructureMembers( dataV.getNetcdfShortName());
    for (DodsV dodsV : dataV.children) {
      StructureMembers.Member m = members.addMember(dodsV.getNetcdfShortName(), null, null, dodsV.getDataType(), dodsV.getShape());
      Array data;
      if ((dodsV.bt instanceof DStructure) || (dodsV.bt instanceof DGrid)) {
        data = makeArrayStructure(dodsV);
      } else if (dodsV.bt instanceof DSequence) {
        data = makeArrayNestedSequence(dodsV);
        m.setShape(data.getShape()); // fix the shape based on the actual data  LOOK
      } else {
        data = Array.factory(dodsV.getDataType(), dodsV.getShapeAll());
      }
      m.setDataArray(data);
      m.setDataObject(data.getIndexIterator()); // for setting values
    }

    return new ArrayStructureMA( members, dataV.getShapeAll());
  }

  private ArrayStructure makeArrayNestedSequence(DodsV dataV) {

    // make the members
    StructureMembers members = new StructureMembers(dataV.getClearName());
    for (DodsV dodsV : dataV.children) {
      members.addMember(dodsV.getClearName(), null, null, dodsV.getDataType(), dodsV.getShape());
    }

    // make the ArraySequence
    // LOOK single nested
    DSequence outerSeq = (DSequence) dataV.parent.bt;
    int outerLength = outerSeq.getRowCount();
    ArraySequenceNested aseq = new ArraySequenceNested( members, outerLength);

    // tell it how long each one is
    String name = dataV.getClearName();
    for (int row=0; row < outerLength; row++) {
      Vector dv = outerSeq.getRow(row);
      for (int j = 0; j < dv.size(); j++) {
        BaseType bt = (BaseType) dv.elementAt(j);
        if (bt.getClearName().equals(name)) {
          DSequence innerSeq = (DSequence) bt;
          int innerLength = innerSeq.getRowCount();
          aseq.setSequenceLength(row, innerLength);
        }
      }
    }
    aseq.finish();

    // ArraySequence makes the inner data arrays; now make iterators for them
    List<StructureMembers.Member> memberList = members.getMembers();
    for (StructureMembers.Member m : memberList) {
      Array data = m.getDataArray();
      m.setDataObject(data.getIndexIterator()); // for setting values
    }

    return aseq;
  }

  // dataV is an array of DStructure: DArray with BaseTypePrimitiveVector, whose values are DStructure
  private void iconvertDataStructureArray(DVector darray, StructureMembers members) throws DAP2Exception {

    List<StructureMembers.Member> mlist = members.getMembers();
    for (StructureMembers.Member member : mlist) {

      // get the Array for this member
      String name = member.getName();
      IndexIterator ii = (IndexIterator) member.getDataObject();

      // loop over each row, fill up the data
      BaseTypePrimitiveVector pv = (BaseTypePrimitiveVector) darray.getPrimitiveVector();
      for (int row = 0; row < pv.getLength(); row++) {
        DStructure ds_data = (DStructure) pv.getValue(row);
        BaseType member_data = ds_data.getVariable(name);
        iconvertData(member_data, ii);
      }
    }
  }

  private void iconvertDataSequenceArray(DSequence dseq, StructureMembers members) throws DAP2Exception {
    for (int row=0; row < dseq.getRowCount(); row++) {
      Vector dv = dseq.getRow(row);
      for (int j = 0; j < dv.size(); j++) {
        BaseType member_data = (BaseType) dv.elementAt(j);
        StructureMembers.Member member = members.findMember( member_data.getEncodedName());
        IndexIterator ii = (IndexIterator) member.getDataObject();
        iconvertData( member_data, ii);
      }
    }
  }

  private void iconvertDataStructure(DConstructor ds, StructureMembers members) throws DAP2Exception {
    List<StructureMembers.Member> mlist = members.getMembers();
    for (StructureMembers.Member member : mlist) {

      // get the Array for this member
      IndexIterator ii = (IndexIterator) member.getDataObject();

      // track down the corresponding DODS member
      String name = member.getName();
      String dodsName = name; // LOOK should be: findDodsName (name);
      if (dodsName == null) {
        throw new DAP2Exception("Cant find dodsName for member variable " + name);
      }

      BaseType bt = ds.getVariable(dodsName);
      iconvertData(bt, ii); // recursively fill up this data array
    }
  }

  private void iconvertData(opendap.dap.BaseType dodsVar, IndexIterator ii) throws DAP2Exception {

    if (dodsVar instanceof DSequence) {
      DSequence dseq = (DSequence) dodsVar;
      StructureData sd =  (StructureData) ii.getObjectNext();
      iconvertDataSequenceArray(dseq, sd.getStructureMembers());

    // this is the "new" 2.0 that (correctly) wraps a Grid array in a Structure
    /* else if ((dodsVar instanceof DStructure) && (ncVar instanceof DODSGrid)){
      DStructure ds = (DStructure) dodsVar;
      try {
        DArray da = (DArray) ds.getVariable(ncVar.getShortName());
        return convertArray(da, ncVar);
      } catch (NoSuchVariableException e) {
        e.printStackTrace();
        return null;
      }
    } */

    /* else

    /* else if (dodsVar instanceof DGrid) { // scalar grid
      DGrid ds = (DGrid) dodsVar;
      try {
        DArray da = (DArray) ds.getVariable(getDODSName( ncVar));
        return convertArray(da, ncVar);
      } catch (NoSuchVariableException e) {
        e.printStackTrace();
        return null;
      }
    } */

    } else if ((dodsVar instanceof DStructure) || (dodsVar instanceof DGrid)) { // nested scalar structure
      DConstructor ds = (DConstructor) dodsVar;
      StructureData sd =  (StructureData) ii.getObjectNext();
      iconvertDataStructure(ds, sd.getStructureMembers());

    // arrays
    } else if (dodsVar instanceof DVector) {
      DVector da = (DVector) dodsVar;
      BaseType bt = da.getPrimitiveVector().getTemplate();

      if (bt instanceof DStructure) {
        StructureData sd =  (StructureData) ii.getObjectNext();
        iconvertDataStructureArray(da, sd.getStructureMembers());

      } else if (bt instanceof DGrid) {
          throw new UnsupportedOperationException();

      } else if (bt instanceof DSequence) {
          throw new UnsupportedOperationException();

      } else {
        iconvertDataPrimitiveArray( da.getPrimitiveVector(), ii);
      }

    // primitive scalars
    } else {
        iconvertDataPrimitiveScalar(dodsVar, ii);
    }

  }

  /*   private Array convertArray(DArray da, Variable ncVar) {
    BaseType elemType = da.getPrimitiveVector().getTemplate();
    if (debugConvertData) System.out.println("  DArray type "+ elemType.getClass().getName());

    if (elemType instanceof DStructure) {  // array of structures LOOK no array of DGrid
      Structure s = (Structure) ncVar;
      StructureMembers members = makeStructureMembers((DStructure) elemType, s);
      ArrayStructureW structArray = new ArrayStructureW( members, makeShape( da));

      // populate it
      IndexIterator ii = structArray.getIndexIterator();
      BaseTypePrimitiveVector pv = (BaseTypePrimitiveVector) da.getPrimitiveVector();
      for (int i=0; i<pv.getLength(); i++) {
        BaseType bt = pv.getValue(i);
        DStructure ds = (DStructure) bt;
        StructureData sd = convertStructureData( structArray, ds.getVariables(), s);
        ii.setObjectNext(sd);
      }
      return structArray;

    } else if (elemType instanceof DString) {
      if (ncVar.getDataType() == DataType.STRING)
        return convertStringArray(da, ncVar);
      else if (ncVar.getDataType() == DataType.CHAR)
        return convertStringArrayToChar(da, ncVar);
      else
        throw new IllegalArgumentException("DODSVariable convertArray invalid dataType= "+ncVar.getDataType()+
            " dodsType= "+elemType.getClass().getName());

    } else {

       // otherwise gotta be a DVector with primitive type
       // create the array, using  DODS internal array so there's no copying
      dods.dap.PrimitiveVector pv = da.getPrimitiveVector();
      Object storage = pv.getInternalStorage();
      //storage = widenArray( pv, storage); // data conversion if needed
      return Array.factory( ncVar.getDataType().getPrimitiveClassType(), makeShape( da), storage);
    }
  } */

  // convert a DODS scalar value
  private void iconvertDataPrimitiveScalar(BaseType dodsScalar, IndexIterator ii) {
    if (dodsScalar instanceof DString) {
      String sval = ((DString) dodsScalar).getValue();
      ii.setObjectNext(sval);

    } else if (dodsScalar instanceof DUInt32) {
      int ival = ((DUInt32) dodsScalar).getValue();
      long lval = DataType.unsignedIntToLong(ival);        // LOOK unsigned
      ii.setLongNext( lval);

    } else if (dodsScalar instanceof DUInt16) {
      short sval = ((DUInt16) dodsScalar).getValue();     // LOOK unsigned
      int ival = DataType.unsignedShortToInt(sval);
      ii.setIntNext (ival);

    } else if (dodsScalar instanceof DFloat32)
      ii.setFloatNext( ((DFloat32) dodsScalar).getValue());
    else if (dodsScalar instanceof DFloat64)
      ii.setDoubleNext( ((DFloat64) dodsScalar).getValue());
    else if (dodsScalar instanceof DInt32)
      ii.setIntNext( ((DInt32) dodsScalar).getValue());
    else if (dodsScalar instanceof DInt16)
      ii.setShortNext( ((DInt16) dodsScalar).getValue());
    else if (dodsScalar instanceof DByte)
      ii.setByteNext( ((DByte) dodsScalar).getValue());
    else
      throw new IllegalArgumentException("DODSVariable extractScalar invalid dataType= " + dodsScalar.getClass().getName());
  }

  // convert a DODS scalar value
  private void iconvertDataPrimitiveArray(PrimitiveVector pv, IndexIterator ii) {
    BaseType bt = pv.getTemplate();

    // set the data value, using scalarIndex from Variable
    if (bt instanceof DString) {
      BaseTypePrimitiveVector bpv = (BaseTypePrimitiveVector) pv;
      for (int row=0; row < bpv.getLength(); row++) {
        DString ds = (DString) bpv.getValue(row);
        ii.setObjectNext( ds.getValue());                     // LOOK CHAR ?
      }

    } else if (bt instanceof DUInt32) {
      UInt32PrimitiveVector bpv = (UInt32PrimitiveVector) pv;
      for (int row=0; row < bpv.getLength(); row++) {
        int ival = bpv.getValue(row);
        long lval = DataType.unsignedIntToLong(ival);        // LOOK unsigned
        ii.setLongNext( lval);
      }
    } else if (bt instanceof DUInt16) {
      UInt16PrimitiveVector bpv = (UInt16PrimitiveVector) pv;
      for (int row=0; row < bpv.getLength(); row++) {
        short sval = bpv.getValue(row);                      // LOOK unsigned
        int ival = DataType.unsignedShortToInt(sval);
        ii.setIntNext (ival);
      }
    } else if (bt instanceof DFloat32) {
      Float32PrimitiveVector bpv = (Float32PrimitiveVector) pv;
      for (int row=0; row < bpv.getLength(); row++)
        ii.setFloatNext (bpv.getValue(row));

    } else if (bt instanceof DFloat64) {
      Float64PrimitiveVector bpv = (Float64PrimitiveVector) pv;
      for (int row=0; row < bpv.getLength(); row++)
        ii.setDoubleNext (bpv.getValue(row));

    } else if (bt instanceof DInt32) {
      Int32PrimitiveVector bpv = (Int32PrimitiveVector) pv;
      for (int row=0; row < bpv.getLength(); row++)
        ii.setIntNext (bpv.getValue(row));

    } else if (bt instanceof DInt16) {
      Int16PrimitiveVector bpv = (Int16PrimitiveVector) pv;
      for (int row=0; row < bpv.getLength(); row++)
        ii.setShortNext (bpv.getValue(row));

    } else if (bt instanceof DByte) {
      BytePrimitiveVector bpv = (BytePrimitiveVector) pv;
      for (int row=0; row < bpv.getLength(); row++)
        ii.setByteNext (bpv.getValue(row));

    } else
      throw new IllegalArgumentException("DODSVariable extractScalar invalid dataType= "+ bt.getClass().getName());
  }

  private Array convertStringArray(DArray dv) {
    opendap.dap.PrimitiveVector pv = dv.getPrimitiveVector();
    BaseTypePrimitiveVector btpv = (BaseTypePrimitiveVector) pv;
    int nStrings = btpv.getLength();
    String[] storage = new String[nStrings];
    for (int i=0; i<nStrings; i++) {
      DString bb = (DString) btpv.getValue(i);
      storage[i] = bb.getValue();
    }
    return Array.factory( DataType.STRING, makeShape( dv), storage);
  }

  // deal with length=1 barfalloney
  // older opendap servers send netcdf char data as Strings of length 1 (!!)
  private Array convertStringArray(Array data, Variable ncVar) {
    String[] storage = (String[]) data.getStorage();
    int max_len = 0;
    for (String s : storage) {
      max_len = Math.max(max_len, s.length());
    }
    if (max_len > 1) return data;

    // below is the length=1 barfalloney
    int count = 0;
    int n = (int) data.getSize();
    char[] charStorage = new char[n];
    for (String s : storage) {
      if (s.length() > 0)
        charStorage[count++] = s.charAt(0);
    }

    // change it to a char (!!). Since its no longer a String, this code wont get called again for this variable.
    ncVar.setDataType( DataType.CHAR);

    // return data thats been changed to chars
    return Array.factory( DataType.CHAR, data.getShape(), charStorage);

    /* if (section == null)
      section = ncVar.getRanges();

    // add the strLen dimension back to the array
    int[] varShape = ncVar.getShape();
    int strLen = varShape[ ncVar.getRank()-1];
    int total = (int) Range.computeSize(section);
    int newSize = total/strLen;
    String[] newStorage = new String[newSize];

    // merge last dimension
    StringBuffer sbuff = new StringBuffer();
    int newCount = 0;
    while (newCount < newSize) {
      int mergeCount = 0;
      sbuff.setLength(0);
      while (mergeCount < strLen) {
        String s = storage[strLen * newCount + mergeCount];
        if (s.length() == 0) break;
        sbuff.append( s);
        mergeCount++;
      }
      newStorage[ newCount++] = sbuff.toString();
    }

    /* List dims = ncVar.getDimensions();
    ncVar.setDimensions( dims.subList(0, ncVar.getRank()-1)); // LOOK is this dangerous or what ???
    int[] newShape = ncVar.getShape();
    return Array.factory( DataType.STRING.getPrimitiveClassType(), newShape, newStorage); */
  }

  private Array convertStringArrayToChar(DArray dv, Variable ncVar, List<Range> section) {
    opendap.dap.PrimitiveVector pv = dv.getPrimitiveVector();
    BaseTypePrimitiveVector btpv = (BaseTypePrimitiveVector) pv;
    int nStrings = btpv.getLength();

    if (section == null)
     section = ncVar.getRanges();
    int[] shape = new Section(section).getShape();
    int total = (int) Index.computeSize(shape);

    //// deal with strlen = 1 silliness
    int max_len = 0;
    for (int i=0; i<nStrings; i++) {
      DString bb = (DString) btpv.getValue(i);
      String val = bb.getValue();
      max_len = Math.max( max_len, val.length());
    }
    if (max_len == 1) {
      char[] charStorage = new char[total];
      for (int i=0; i<nStrings; i++) {
        DString bb = (DString) btpv.getValue(i);
        String val = bb.getValue();
        charStorage[i] =  (val.length() > 0) ? val.charAt(0) : 0;
      }
      // return data thats been changed to chars
      return Array.factory( DataType.CHAR, shape, charStorage);
    }

    //// this is the "normal case", where String[rank] converted to char[rank+1]

    int[] varShape = ncVar.getShape();
    int strLen = varShape[ ncVar.getRank()-1];

    char[] storage = new char[total];
    int pos = 0;
    for (int i=0; i<nStrings; i++) {
      DString bb = (DString) btpv.getValue(i);
      String val = bb.getValue();
      int len = Math.min( val.length(), strLen);
      for (int k=0; k<len; k++)
        storage[pos+k] = val.charAt(k);
      pos += strLen;
    }

    return Array.factory( DataType.CHAR, shape, storage);

        /* add the strLen dimension back to the array
    ArrayList fullSection = (section == null) ? (ArrayList) ncVar.getRanges() : new ArrayList( section);
    try {
      Range.appendShape( fullSection, strLen);
    } catch (InvalidRangeException e) {
      e.printStackTrace();  // cant happen ??
      throw new IllegalStateException("convertStringArrayToChar strLen = "+strLen+" on variable "+ncVar.getName());
    } */
  }

  private Array convertStringToChar(Array data, Variable ncVar) {
    String s = (String) data.getObject(Index.scalarIndexImmutable);

    int total = (int) ncVar.getSize();
    char[] storage = new char[total];

    int len = Math.min( s.length(), total);
    for (int k=0; k<len; k++)
      storage[k] = s.charAt(k);

    return Array.factory( DataType.CHAR, ncVar.getShape(), storage);
  }

  private int[] makeShape( opendap.dap.DArray dodsArray) {
    int count = 0;
    Enumeration enumerate = dodsArray.getDimensions();
    while (enumerate.hasMoreElements()) {
      count++;
      enumerate.nextElement();
    }

    int[] shape = new int[count];
    enumerate = dodsArray.getDimensions();
    count = 0;
    while (enumerate.hasMoreElements()) {
      opendap.dap.DArrayDimension dad = (opendap.dap.DArrayDimension) enumerate.nextElement();
      shape[count++] = dad.getSize();
    }

    return shape;
  }

  /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  //////////////////////////////////////////////////////////////////////////////////////
  /*
   * Make an ArrayStructureMA that can hold all the data in the Structure. Nested Structures
   *  are handled by nested ArrayStructureMA.
   * @param s the Structure
   * @return an ArrayStructureMA
   *
  private ArrayStructure makeArrayStructure(Structure s) {
    int n = (int) s.getSize();
    Range outerRange;
    try {
      outerRange = new Range(0, n-1);
    } catch (InvalidRangeException e) {
      logger.error("makeArrayStructure", e);
      throw new IllegalArgumentException("Structure "+s.getName()+" has length = 0");
    }

    StructureMembers members = s.makeStructureMembers();
    ArrayStructureMA asma = new ArrayStructureMA( members, s.getShape());
    List mlist = members.getMembers();
    for (int i = 0; i < mlist.size(); i++) {
      StructureMembers.Member member = (StructureMembers.Member) mlist.get(i);
      Variable v = s.findVariable( member.getName());
      Array data;
      /* if (v.getDataType() == DataType.STRUCTURE)
        data = makeArrayStructure((Structure) v);
      else *
      ArrayList ranges = new ArrayList( Range.factory(v.getShape()));
      ranges.add(0, outerRange);
      data = Array.factory(v.getDataType(), Range.getShape( ranges));
      member.setDataObject( data);
    }
    return asma;
  }

  private ArrayStructure makeArrayStructure2(Structure s) {
    StructureMembers members = s.makeStructureMembers();
    List memberList = members.getMembers();
    for (int i = 0; i < memberList.size(); i++) {
      StructureMembers.Member m = (StructureMembers.Member) memberList.get(i);
      Array data;
      if (m.getDataType() == DataType.STRUCTURE) {
        Structure nested = (Structure) s.findVariable( m.getName());
        data = makeArrayStructure2(nested);
      } else {
        Variable nested = s.findVariable( m.getName());
        List ranges = nested.getRangesAll();
        data = Array.factory(m.getDataType(), Range.getShape(ranges));
      }
      m.setDataObject( data);
    }

    List ranges = s.getRangesAll();
    ArrayStructureMA as = new ArrayStructureMA( members, Range.getShape(ranges));
    return as;
  }

  private void convertDataStructureArray(DArray darray, DStructure ds, ArrayStructure structArray) throws NoSuchVariableException {
    StructureMembers members = structArray.getStructureMembers();
    List mlist = members.getMembers();
    for (int i = 0; i < mlist.size(); i++) {

      // get the Array for this member
      StructureMembers.Member member = (StructureMembers.Member) mlist.get(i);
      Array data = (Array) member.getDataObject();

      // track down the corresponding DODS member
      String name = member.getName();
      BaseType bt = null;
      try {
        bt = ds.getVariable( name);
      } catch (NoSuchVariableException e) {
        logger.error("Cant find Dods Varable "+name, e);
        continue;
      }

      // loop over each row, fill up the data
      IndexIterator ii = data.getIndexIterator();
      BaseTypePrimitiveVector pv = (BaseTypePrimitiveVector) darray.getPrimitiveVector();
      for (int row=0; row < pv.getLength(); row++) {
        DStructure ds_data = (DStructure) pv.getValue(row);
        BaseType member_data = ds_data.getVariable( name);
        convertData( member_data, ii);
      }

    }
  } */

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

}


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

package thredds.inventory.bdb;

import com.sleepycat.je.*;

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

import thredds.inventory.MFile;
import thredds.inventory.StoreKeyValue;
import ucar.nc2.constants.CDM;

/**
 * MetadataManager using Berkeley DB Java Edition.
 * Single environment, with multiple databases.
 * All threads share one environment; multiple processes can only have one writer at a time.
 * Each collection is a "database".
 * Each database has a set of key/value pairs.
 * default root dir is ${user.home}/.unidata/bdb
 * default TDS uses {tomcat_home}/content/thredds/cache/collection
 *
 * @author caron
 * @since Aug 20, 2008
 */
public class MetadataManager implements StoreKeyValue {
  private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(MetadataManager.class);
  private static final String UTF8 = "UTF-8";

  private static String root = null;
  private static long maxSizeBytes = 0;
  private static int jvmPercent = 2;
  private static Environment myEnv = null;
  private static List<MetadataManager> openDatabases = new ArrayList<>();
  private static boolean readOnly = false;
  private static boolean debug = false;

  //private static boolean debugDelete = false;

  static {
    String home = System.getProperty("user.home");

    if (home == null)
      home = System.getProperty("user.dir");

    if (home == null)
      home = ".";

    root = home + "/.unidata/bdb/";
  }

  static public synchronized void setCacheDirectory(String dir, long _maxSizeBytes, int _jvmPercent) {
    root = dir;
    maxSizeBytes = _maxSizeBytes;
    jvmPercent = _jvmPercent;
  }

  static private synchronized void setup() throws DatabaseException {
    if (myEnv != null) return; // someone else did it
    logger.info("try to open bdb");

    EnvironmentConfig myEnvConfig = new EnvironmentConfig();
    myEnvConfig.setReadOnly(false);
    myEnvConfig.setAllowCreate(true);
    myEnvConfig.setSharedCache(true);

    if (maxSizeBytes > 0)
      myEnvConfig.setCacheSize(maxSizeBytes);
    else
      myEnvConfig.setCachePercent(jvmPercent);

    File dir = new File(root);
    if (!dir.exists() && !dir.mkdirs())
      logger.warn("MetadataManager failed to make directory " + root);

    try {
      myEnv = new Environment(dir, myEnvConfig); // LOOK may want to try multiple Environments
      readOnly = false;

     } catch (com.sleepycat.je.EnvironmentLockedException e) {
      // another process has it open: try read-only
      logger.warn("MetadataManager failed to open directory {}, try read-only", root);
      logger.error("failed to open bdb", e);
      logger.warn("myEnvConfig {}", myEnvConfig);

      readOnly = true;
      myEnvConfig.setReadOnly(true);
      myEnvConfig.setAllowCreate(false);
      myEnv = new Environment(dir, myEnvConfig);
    }
    logger.info("MetadataManager: open bdb at root "+root+" readOnly = " +readOnly);

    /* primary
    DatabaseConfig dbConfig = new DatabaseConfig();
    dbConfig.setAllowCreate(true);
    dbConfig.setDeferredWrite(true);
    database = myEnv.openDatabase(null, "primaryDatabase", dbConfig);

    /* secondary
    SecondaryKeyCreator keyCreator = new SecondaryKeyCreator(new DataBinding());
    SecondaryConfig secConfig = new SecondaryConfig();
    secConfig.setAllowCreate(!readOnly);
    secConfig.setSortedDuplicates(!readOnly);
    secConfig.setKeyCreator(keyCreator);

    secondary = myEnv.openSecondaryDatabase(null, "secDatabase", database, secConfig);  */
  }

  // Close all databases and environment
  // this is called on TDS shutdown and reinit
  static synchronized public void closeAll() {

    List<MetadataManager> closeDatabases = new ArrayList<>(openDatabases);
    for (MetadataManager mm : closeDatabases) {
      if (debug) System.out.println("  close database " + mm.collectionName);
      mm.close();
    }
    openDatabases = new ArrayList<>(); // empty

    if (myEnv != null) {
      try {
        // Finally, close the store and environment.
        myEnv.close();
        myEnv = null;
        logger.info("closed bdb caching");

      } catch (DatabaseException dbe) {
        logger.error("Error closing bdb: ", dbe);
      }
    }
  }

  static public void showEnvStats(Formatter f) {
    if (myEnv == null)
      setup();

    try {
      EnvironmentStats stats = myEnv.getStats(null);
      f.format("EnvironmentStats%n%s%n", stats);

      f.format("%nDatabaseNames%n");
      for (String dbName : myEnv.getDatabaseNames()) {
        f.format(" %s%n", dbName);
      }
    } catch (DatabaseException e) {
      e.printStackTrace();
    }
  }

  static public synchronized String getCacheLocation() {
    return root;
  }

  static public void sync() {
    if (myEnv != null)
      myEnv.sync();
  }

  static public List<String> getCollectionNames() {
    if (myEnv == null)
      setup();
    return myEnv.getDatabaseNames();
  }

  static public synchronized void deleteCollection(String collectionName) throws Exception {
    List<MetadataManager> closeDatabases = new ArrayList<>(openDatabases);

    // close any open handles
    for (MetadataManager mm : closeDatabases) {
      if (mm.collectionName.equals(collectionName)) {
        if (mm.database != null) {
          mm.database.close();
          mm.database = null; // allow it to reopen next time its called
          openDatabases.remove(mm);
        }
      }
    }
    myEnv.removeDatabase(null, collectionName);
  }

  static public void delete(String collectionName, String key) {
    try {
      MetadataManager mm = new MetadataManager(collectionName);
      mm.delete(key);
      mm.close();
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  static public StoreKeyValue.Factory getFactory() {
    return new MetadataManagerFactory();
  }

  static private class MetadataManagerFactory implements StoreKeyValue.Factory {

    @Override
    public StoreKeyValue open(String name) {
      try {
        return new MetadataManager(name);
      } catch (IOException ioe) {
        throw new RuntimeException(ioe);
      }
    }
  }

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

  private String collectionName;
  private Database database;
  //private SecondaryDatabase secondary;
  //private Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
  //private DateFormatter dateFormatter = new DateFormatter();

  public MetadataManager(String collectionName) throws DatabaseException, IOException {
    this.collectionName = collectionName;

    // fail fast
    if (myEnv == null) {
      setup();
    }
  }

  // assumes only one open at a time; could have MetadataManagers share open databases
  private synchronized void openDatabase() {
    if (database != null) return;
    DatabaseConfig dbConfig = new DatabaseConfig();
    dbConfig.setReadOnly(readOnly);
    dbConfig.setAllowCreate(!readOnly);
    if (!readOnly)
      dbConfig.setDeferredWrite(true);
    database = myEnv.openDatabase(null, collectionName, dbConfig);

    openDatabases.add(this);
  }

  public void put(String key, String value) {
    if (readOnly) return;
    openDatabase();
    try {
      database.put(null, new DatabaseEntry(key.getBytes(UTF8)), new DatabaseEntry(value.getBytes(UTF8)));
    } catch (UnsupportedEncodingException e) {
      throw new RuntimeException(e.getMessage());
    }
  }

  public void put(byte[] key, byte[] value) {
    if (readOnly) return;
    openDatabase();
    database.put(null, new DatabaseEntry(key), new DatabaseEntry(value));
  }

  public void put(String key, byte[] value) {
    if (readOnly) return;
    openDatabase();
    try {
      database.put(null, new DatabaseEntry(key.getBytes(UTF8)), new DatabaseEntry(value));
    } catch (UnsupportedEncodingException e) {
      throw new RuntimeException(e.getMessage());
    }
  }

  public byte[] get(byte[] key) {
    openDatabase();
    DatabaseEntry value = new DatabaseEntry();
    database.get(null, new DatabaseEntry(key), value, LockMode.DEFAULT);
    return value.getData();
  }

  public byte[] getBytes(String key) {
    openDatabase();
    try {
      DatabaseEntry value = new DatabaseEntry();
      database.get(null, new DatabaseEntry(key.getBytes(UTF8)), value, LockMode.DEFAULT);
      return value.getData();
    } catch (UnsupportedEncodingException e) {
      throw new RuntimeException(e);
    }
  }

  public String get(String key) {
    openDatabase();
    try {
      DatabaseEntry value = new DatabaseEntry();
      OperationStatus status = database.get(null, new DatabaseEntry(key.getBytes(UTF8)), value, LockMode.DEFAULT);
      if (status == OperationStatus.SUCCESS)
        return new String(value.getData(), UTF8);
      else
        return null;
    } catch (UnsupportedEncodingException e) {
      throw new RuntimeException(e);
    }
  }

  public void delete(String theKey) {
    if (readOnly) {
      logger.warn("Cant delete - readOnly mode");
      return;
    }

    openDatabase();
    try {
      DatabaseEntry entry = new DatabaseEntry(theKey.getBytes(CDM.utf8Charset));
      database.delete(null, entry);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  public void delete(Map<String, MFile> current) {
    if (readOnly) {
      logger.warn("Cant delete - readOnly mode");
      return;
    }

    openDatabase();
    List<DatabaseEntry> result = new ArrayList<>();
    Cursor myCursor = null;
    try {
      myCursor = database.openCursor(null, null);
      DatabaseEntry foundKey = new DatabaseEntry();
      DatabaseEntry foundData = new DatabaseEntry();

      int count = 0;
      while (myCursor.getNext(foundKey, foundData, LockMode.DEFAULT) == OperationStatus.SUCCESS) {
        String key = new String(foundKey.getData(), UTF8);
        int pos = key.indexOf('#');
        if (pos > 0) {
          String filename = key.substring(0, pos);
          if (null == current.get(filename)) {
            logger.debug("{} not in current - want to delete from bdb", filename);
            result.add(new DatabaseEntry(foundKey.getData()));
            count++;
          } else {
            logger.debug("{} is in current", filename);
          }
        }
      }

      int countDelete = 0;
      for (DatabaseEntry entry : result) {
        OperationStatus status = database.delete(null, entry);
        String key = new String(entry.getData(), UTF8);
        logger.debug("{} deleted {}", status, key);
        countDelete++;
      }
      logger.info("{} #files deleted = {}", collectionName, countDelete);

    } catch (UnsupportedOperationException | UnsupportedEncodingException e) {
      logger.error("Trying to delete " + collectionName, e);

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

  }

  public void close() {
    if (database != null) {
      database.close();
      openDatabases.remove(this);
      database = null;
    }
  }

  public void showStats(Formatter f) {
    openDatabase();
    try {
      DatabaseStats dstats = database.getStats(null);
      f.format("primary stats %n%s%n", dstats);

    } catch (DatabaseException e) {
      e.printStackTrace();
    }
  }

  public List<KeyValue> getContent() throws DatabaseException, UnsupportedEncodingException {
    openDatabase();
    List<KeyValue> result = new ArrayList<>();
    Cursor myCursor = null;
    try {
      myCursor = database.openCursor(null, null);
      DatabaseEntry foundKey = new DatabaseEntry();
      DatabaseEntry foundData = new DatabaseEntry();

      //int count = 0;
      while (myCursor.getNext(foundKey, foundData, LockMode.DEFAULT) == OperationStatus.SUCCESS) {
        String key = new String(foundKey.getData(), UTF8);
        String data = new String(foundData.getData(), UTF8);
        result.add(new KeyValue(key, data));
        //System.out.printf("key = %s; data = %s %n", key, data);
        //count++;
      }
      //System.out.printf("count = %d %n", count);
    } finally {
      if (null != myCursor)
        myCursor.close();
    }

    return result;
  }

  public static class KeyValue {
    public String key;
    public String value;

    KeyValue(String key, String value) {
      this.key = key;
      this.value = value;
    }
  }

  public static void main(String args[]) throws Exception {
    MetadataManager indexer = new MetadataManager("dummy");
    indexer.showStats(new Formatter(System.out));
    MetadataManager.closeAll();
  }

}


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

package thredds.ui.datatype.prefs;

import ucar.nc2.ui.widget.MultilineTooltip;
import ucar.nc2.units.DateType;
import ucar.util.prefs.PersistenceManager;
import ucar.util.prefs.ui.FldInputVerifier;

import javax.swing.*;
import javax.swing.text.JTextComponent;

/**
 * Input field for thredds.datatype.DateType, part of ucar.util.prefs.ui.
 *
 * @author John Caron
 * @see ucar.util.prefs.ui.Field
 */

public class DateField extends ucar.util.prefs.ui.Field {

  protected JTextComponent tf;

  /**
   * Constructor.
   *
   * @param name      of the field; must be unique within the store
   * @param label     to display to the user
   * @param defValue  default value to start with.
   * @param storeData store/fetch data from here, may be null.
   */
  public DateField(String name, String label, DateType defValue, PersistenceManager storeData) {
    super(name, label, storeData);
    validValue = getStoreValue(defValue);
    tf = new JTextField() {
      @Override
      public JToolTip createToolTip() {
        return new MultilineTooltip();
      }
    };
    tf.setToolTipText("Formats:\n 'present'\n CCYY-MM-DD\n CCYY-MM-DDThh:mm:ss\n udunits date string");
    tf.setInputVerifier(new FldInputVerifier(tf, this));

    if (validValue != null)
      tf.setText(validValue.toString());

    finish();
  }

  // return the editing JComponent
  @Override
  public JComponent getEditComponent() {
    return tf;
  }

  /**
   * See if edit value is valid, put error message in buff.
   */
  @Override
  protected boolean _validate(StringBuffer buff) {
    try {
      new DateType(tf.getText(), null, null);
      return true;
    } catch (java.text.ParseException e) {
      if (null != buff) buff.append(name).append(": ").append(e.getMessage());
      return false;
    }
  }

  // get current value from editComponent
  @Override
  protected Object getEditValue() {
    try {
      return new DateType(tf.getText(), null, null);
    } catch (java.text.ParseException e) {
      return null;
    }
  }

  // get current value
  public DateType getDate() {
    return (DateType) getValue();
  }

  // set current value of editComponent
  @Override
  protected void setEditValue(Object value) {
    if (value == null)
      tf.setText("");
    else
      tf.setText(value.toString());
  }

  @Override
  protected void setStoreValue(Object value) {
    if (storeData != null) {
      if (value != null)
        storeData.putObject(name, new DateType((DateType) value));
    }
  }

  @Override
  protected Object getStoreValue(Object defValue) {
    Object value = defValue;
    if (storeData != null) {
      Object value2 = storeData.getObject(name);
      if (value2 != null)
        value = value2;
    }
    if (value == null) return null;
    return new DateType((DateType) value);
  }
}


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

package ucar.nc2.time;

import org.joda.time.Period;

import javax.annotation.concurrent.Immutable;

/**
 * A replacement for ucar.nc2.units.TimeDuration. incomplete.
 *
 *
 * A Duration is a fixed number of seconds.
 * I think thats wrong: a CalendarDuration should be a integer and a CalendarPeriod.
 * Optionally, a CalendarPeriod could have an integer value.
 *
 * Implements the thredds "duration" XML element type: specifies a length of time.
 * This is really the same as a ucar.nc2.units.TimeUnit, but it allows xsd:duration syntax as well as udunits syntax.
 * It also keeps track if the text is empty.
 *

 * A duration can be one of the following:
 *

 *
 a valid udunits string compatible with "secs"
 *
 an xsd:duration type specified in the following form "PnYnMnDTnHnMnS" where:
 *

 *
P indicates the period (required)
 *
nY indicates the number of years
 *
nM indicates the number of months
 *
nD indicates the number of days
 *
T indicates the start of a time section (required if you are going to specify hours, minutes, or seconds)
 *
nH indicates the number of hours
 *
nM indicates the number of minutes
 *
nS indicates the number of seconds
 *

 *

 *
 *

 * change to JSR-310 javax.time.Duration when ready
 * relation to javax.xml.datatype ?
 *
 * @author john caron
 * @see "http://www.unidata.ucar.edu/projects/THREDDS/tech/catalog/InvCatalogSpec.html#durationType"
 */

@Immutable
public class CalendarDuration {

  /**
   * Convert a time udunit string
   * @param value number of units
   * @param udunit msec, sec, minute, hour, hr, day, week, month, year (plural form ok)
   * @return joda Period
   */
  static org.joda.time.Period convertToPeriod(int value, String udunit) {
    if (udunit.endsWith("s")) udunit = udunit.substring(0, udunit.length()-1);

    switch (udunit) {
      case "msec":
        return Period.millis(value);
      case "sec":
        return Period.seconds(value);
      case "minute":
        return Period.minutes(value);
      case "hour":
      case "hr":
        return Period.hours(value);
      case "day":
        return Period.days(value);
      case "week":
        return Period.weeks(value);
      case "month":
        return Period.months(value);
      case "year":
        return Period.years(value);
    }

    throw new IllegalArgumentException("cant convert "+ udunit +" to Joda Period");
  }

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

  //private final String text;
  private final CalendarPeriod.Field timeUnit;
  //private final boolean isBlank;
  private final double value;
  //private org.joda.time.Period jperiod;

  public CalendarDuration(int value, CalendarPeriod.Field timeUnit) {
    this.value = value;
    this.timeUnit = timeUnit;
  }

  public CalendarPeriod.Field getTimeUnit() {
    return timeUnit;
  }

  public static CalendarDuration fromUnitString(String unitString) {
    return new CalendarDuration(1, CalendarPeriod.fromUnitString(unitString));
  }

  /*
   * Construct from 1) udunit time udunit string, 2) xsd:duration syntax, 3) blank string.
   *
   * @param text parse this text.
   * @throws java.text.ParseException if invalid text.
   *
  public CalendarDuration(String text) throws java.text.ParseException {
/*    TimeUnit tunit = null;
    text = (text == null) ? "" : text.trim();
    this.text = text;
    this.isBlank = (text == null) || text.length() == 0;

    // see if its blank
    if (isBlank) {
      try {
        tunit = new TimeUnit("1 sec");
      } catch (Exception e) { // cant happen
      }

    } else {
      // see if its a udunits string
      try {
        tunit = new TimeUnit(text);

        if (debug) System.out.println(" set time udunit= " + timeUnit);
      }

      catch (Exception e) {
        // see if its a xsd:duration
        /*
         * A time span as defined in the W3C XML Schema 1.0 specification:
         * "PnYnMnDTnHnMnS, where nY represents the number of years, nM the number of months, nD the number of days,
         * 'T' is the date/time separator, nH the number of hours, nM the number of minutes and nS the number of seconds.
         * The number of seconds can include decimal digits to arbitrary precision."
         *
        try {
          javax.xml.datatype.DatatypeFactory factory = javax.xml.datatype.DatatypeFactory.newInstance();
          javax.xml.datatype.Duration d = factory.newDuration(text);
          long secs = d.getTimeInMillis(new Date()) / 1000;
          tunit = new TimeUnit(secs + " secs");
        } catch (Exception e1) {
          throw new java.text.ParseException(e1.getMessage(), 0);
        }
      }
    }

    timeUnit = tunit;
    duration = tunit.toDurationStandard();
  }*/

  /**
   * Get the duration in natural units, ie units of getTimeUnit()
   * @return the duration in natural units
   */
  public double getValue() {
    return value;
  }

  /**
   * Get the duration in seconds
   * @return the duration in seconds
   *
  public double getValueInMillisecs() {
    if (timeUnit == CalendarPeriod.Millisec)
      return value;
    else if (timeUnit == CalendarPeriod.Second)
      return 1000 * value;
    else if (timeUnit == CalendarPeriod.Minute)
      return 60 * 1000 * value;
    else if (timeUnit == CalendarPeriod.Hour)
      return 60 * 60 * 1000 * value;
    else if (timeUnit == CalendarPeriod.Day)
      return 24 * 60 * 60 * 1000 * value;
    else if (timeUnit == CalendarPeriod.Month)
      return 30.0 * 24.0 * 60.0 * 60.0 * 1000.0 * value;
    else if (timeUnit == CalendarPeriod.Year)
      return 365.0 * 24.0 * 60.0 * 60.0 * 1000.0 * value;

    else throw new IllegalStateException();
  }

  public double getValueInSeconds() {
    if (timeUnit == CalendarPeriod.Millisec)
      return value/1000;
    else if (timeUnit == CalendarPeriod.Second)
      return value;
    else if (timeUnit == CalendarPeriod.Minute)
      return 60 * value;
    else if (timeUnit == CalendarPeriod.Hour)
      return 60 * 60 * value;
    else if (timeUnit == CalendarPeriod.Day)
      return 24 * 60 * 60 * value;
    else if (timeUnit == CalendarPeriod.Month)
      return 30.0 * 24.0 * 60.0 * 60.0 * value;
    else if (timeUnit == CalendarPeriod.Year)
      return 365.0 * 24.0 * 60.0 * 60.0 * value;

    else throw new IllegalStateException();
  }

  public double getValueInHours() {
    if (timeUnit == CalendarPeriod.Millisec)
      return value/1000/60/60;
    else if (timeUnit == CalendarPeriod.Second)
      return value/60/60;
    else if (timeUnit == CalendarPeriod.Minute)
      return value/60;
    else if (timeUnit == CalendarPeriod.Hour)
      return value;
    else if (timeUnit == CalendarPeriod.Day)
      return 24 * value;
    else if (timeUnit == CalendarPeriod.Month)
      return 30.0 * 24.0 * value;
    else if (timeUnit == CalendarPeriod.Year)
      return 365.0 * 24.0 * value;

    else throw new IllegalStateException();
  }

  public double getValueInDays() {
    if (timeUnit == CalendarPeriod.Millisec)
      return value/1000/60/60/24;
    else if (timeUnit == CalendarPeriod.Second)
      return value/60/60/24;
    else if (timeUnit == CalendarPeriod.Minute)
      return value/60/24;
    else if (timeUnit == CalendarPeriod.Hour)
      return value/24;
    else if (timeUnit == CalendarPeriod.Day)
      return value;
    else if (timeUnit == CalendarPeriod.Month)
      return 30.0 * value;
    else if (timeUnit == CalendarPeriod.Year)
      return 365.0 * value;

    else throw new IllegalStateException();
  } */

  /*
   * If this is a blank string
   * @return true if this is a blank string
   *
  public boolean isBlank() {
    return isBlank;
  }

  /*
   * Get the String text
   * @return the text
   *
  public String getText() {
    return text == null ? timeUnit.toString() : text;
  } */

  /**
   * String representation
   * @return getText()
   */
  public String toString() {
    return timeUnit.toString();
  }

  /*
   * Override to be consistent with equals
   *
  public int hashCode2() {
    return isBlank() ? 0 : (int) getValueInSeconds();
  }

  /*
   * TimeDurations with same value in seconds are equals
   *
  public boolean equals2(Object o) {
    if (this == o)
      return true;
    if (!(o instanceof CalendarDuration))
      return false;

    CalendarDuration to = (CalendarDuration) o;
    return to.getValueInMillisecs() == getValueInMillisecs();
  } */

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    CalendarDuration that = (CalendarDuration) o;

    if (Double.compare(that.value, value) != 0) return false;
    return timeUnit == that.timeUnit;
  }

  @Override
  public int hashCode() {
    int result;
    long temp;
    result = timeUnit != null ? timeUnit.hashCode() : 0;
    temp = value != +0.0d ? Double.doubleToLongBits(value) : 0L;
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    return result;
  }

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

static private void test(String unit, String result) throws Exception {
  org.joda.time.Period jp = convertToPeriod(1, unit);
  assert jp != null;
  System.out.printf("%s == %s%n", unit, jp);
  assert jp.toString().equals(result) : jp.toString()+" != "+ result;
}

static public void main(String args[]) throws Exception {
  test("sec", "PT1S");
  test("secs", "PT1S");
  test("minute", "PT1M");
  test("minutes", "PT1M");
  test("hour", "PT1H");
  test("hours", "PT1H");
  test("hr", "PT1H");
  test("day", "P1D");
  test("days", "P1D");
  test("week", "P7D");
  test("weeks", "P7D");
  test("month", "P1M");
  test("months", "P1M");
  test("year", "P1Y");
  test("years", "P1Y");
}

}


package ucar.nc2.grib.grib2.table;

import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import javax.annotation.Nullable;

/**
 * ECMWF code tables read from resources/grib2/ecmwf/tables/<latest>.
 * EcmwfCodeTableCompare is used to compare with WMO.
 * Notes on important differences, that should get reflected in Grib2Customizer:
 <pre>

 1) Code table 1.0 - GRIB master tables version number
    Ecmwf version 21 discipline 1 category 0 (resources/grib2/ecmwf/tables/21/1.0.table)
      p1=       255 Missing
      p2=       255 Master tables not used. Local table entries and local templates may use the entire range of the table, not just
                    those sections marked Reserved for local used.

 2) Code table 3.1 - Grid definition template number
    Ecmwf version 21 discipline 3 category 1 (resources/grib2/ecmwf/tables/21/3.1.table)
      WMO 3.1 missing EcmwfEntry{codeValue=12, name=Transverse Mercator}
      WMO 3.1 missing EcmwfEntry{codeValue=130, name=Irregular latitude/longitude grid}

 3) Code table 4.0 - Product definition template number
    Ecmwf version 21 discipline 4 category 0 (resources/grib2/ecmwf/tables/21/4.0.table)
     WMO 4.0 missing EcmwfEntry{codeValue=311, name=Satellite product auxiliary information}
     WMO 4.0 missing EcmwfEntry{codeValue=40033, name=Individual ensemble forecast, control and perturbed, at a horizontal level or in a horizontal layer at a point in time for simulated}
     WMO 4.0 missing EcmwfEntry{codeValue=40034, name=Individual ensemble forecast, control and perturbed, at a horizontal level or in a horizontal layer, in a continuous or non-continuous interval for simulated}
     WMO 4.0 missing EcmwfEntry{codeValue=50001, name=Forecasting Systems with Variable Resolution in a point in time}
     WMO 4.0 missing EcmwfEntry{codeValue=50011, name=Forecasting Systems with Variable Resolution in a continous or non countinous time interval}

 4) Code table 4.230 - Atmospheric chemical constituent type
    Ecmwf version 21 discipline 4 category 230 (resources/grib2/ecmwf/tables/21/4.230.table)
      WMO 4.230 missing EcmwfEntry{codeValue=0, name=Ozone O3}
      ... (all of them?)
    Same with
      Code table 4.233 - Aerosol type

 5) Code table 5.0 - Data representation template number
    Ecmwf version 21 discipline 5 category 0 (resources/grib2/ecmwf/tables/21/5.0.table)
     WMO 5.0 missing EcmwfEntry{codeValue=6, name=Grid point data - simple packing with pre-processing}
     WMO 5.0 missing EcmwfEntry{codeValue=40000, name=JPEG2000 Packing}
     WMO 5.0 missing EcmwfEntry{codeValue=40010, name=PNG pacling}
     WMO 5.0 missing EcmwfEntry{codeValue=50000, name=Sperical harmonics ieee packing}
     WMO 5.0 missing EcmwfEntry{codeValue=50001, name=Second order packing}
     WMO 5.0 missing EcmwfEntry{codeValue=50002, name=Second order packing}

 6) Code table 6.0 - Bit map indicator
    Ecmwf version 21 discipline 6 category 0 (resources/grib2/ecmwf/tables/21/6.0.table)
      WMO 6.0 missing EcmwfEntry{codeValue=1, name=A bit map pre-determined by the originating/generating centre applies to
              this product and is not specified in this Section}

 7)
   No WMO table that matches ECMWF table 4.192
   No WMO table that matches ECMWF table 5.40000
   No WMO table that matches ECMWF table 5.50002
   Unparsed table stepType.table
 </pre>
 */
public class EcmwfCodeTable implements Grib2CodeTableInterface {
  private static final boolean debugOpen = false;
  private static final boolean debug = false;
  private static final String PATH = "resources/grib2/ecmwf/tables";

  private String title;
  private String source;
  private String tableName;

  private final int version, discipline, category;
  private final ImmutableMap<Integer, Entry> paramMap;

  public static EcmwfCodeTable factory(int version, int discipline, int category) {
    try {
      return new EcmwfCodeTable(version, discipline, category);
    } catch (Exception e) {
      return null;
    }
  }

  private EcmwfCodeTable(int version, int discipline, int category) throws IOException {
    this.version = version;
    this.discipline = discipline;
    this.category = category;

    this.paramMap = this.readTable(getTablePath());
  }

  @Override
  public String getName() {
    return String.format("Ecmwf version %d discipline %d category %d (%s)", version, discipline, category, getTablePath());
  }

  @Override
  public String getShortName() {
    return String.format("Ecmwf version %d (%d-%d)", version, discipline, category);
  }

  @Override
  public ImmutableList<Entry> getEntries() {
    return paramMap.values().stream().sorted().collect(ImmutableList.toImmutableList());
  }

  @Override
  @Nullable
  public Entry getEntry(int code) {
    return paramMap.get(code);
  }

  private String getTablePath() {
    return String.format("%s/%d/%d.%d.table", PATH, version, discipline, category);
  }

  /*
  # Code table 4.2 - Parameter number by product discipline and parameter category
  0 0 Estimated precipitation (kg m-2)
  1 1 Instantaneous rain rate (kg m-2 s-1)
  2 2 Cloud top height (m)
  3 3 Cloud top height quality indicator (Code table 4.219)
  4 4 Estimated u-component of wind (m/s)
  5 5 Estimated v-component of wind (m/s)
  6 6 Number of pixel used (Numeric)
  7 7 Solar zenith angle (deg)
  8 8 Relative azimuth angle (deg)
  9 9 Reflectance in 0.6 micron channel (%)
  10 10 Reflectance in 0.8 micron channel (%)
  11 11 Reflectance in 1.6 micron channel (%)
  12 12 Reflectance in 3.9 micron channel (%)
  13 13 Atmospheric divergence (/s)
  14 14 Cloudy brightness temperature (K)
  15 15 Clear-sky brightness temperature (K)
  16 16 Cloudy radiance (with respect to wave number) (W m-1 sr-1)
  17 17 Clear-sky radiance (with respect to wave number) (W m-1 sr-1)
  18 18 Reserved
  19 19 Wind speed (m/s)
  20 20 Aerosol optical thickness at 0.635 um
  21 21 Aerosol optical thickness at 0.810 um
  22 22 Aerosol optical thickness at 1.640 um
  23 23 Angstrom coefficient
  # 24-26 Reserved
  27 27 Bidirectional reflectance factor (numeric)
  28 28 Brightness temperature (K)
  29 29 Scaled radiance (numeric)
  # 30-191 Reserved
  # 192-254 Reserved for local use
  255 255 Missing
   */
  private ImmutableMap<Integer, Entry> readTable(String path) throws IOException {
    ImmutableMap.Builder<Integer, Entry> builder = ImmutableMap.builder();

    if (debugOpen) {
      System.out.printf("readEcmwfTable path= %s%n", path);
    }

    ClassLoader cl = Grib2TableConfig.class.getClassLoader();
    try (InputStream is = cl.getResourceAsStream(path)) {
      if (is == null) {
        throw new IllegalStateException("Cant find " + path);
      }
      try (BufferedReader dataIS = new BufferedReader(
          new InputStreamReader(is, Charset.forName("UTF8")))) {
        int count = 0;
        while (true) {
          String line = dataIS.readLine();
          if (line == null) {
            break;
          }
          if (line.startsWith("#") || line.trim().length() == 0) {
            continue;
          }
          count++;

          int posBlank1 = line.indexOf(' ');
          int posBlank2 = line.indexOf(' ', posBlank1 + 1);
          int lastParen = line.lastIndexOf('(');

          String num1 = line.substring(0, posBlank1).trim();
          String num2 = line.substring(posBlank1 + 1, posBlank2);
          String desc = (lastParen > 0) ? line.substring(posBlank2 + 1, lastParen).trim()
              : line.substring(posBlank2 + 1).trim();

          if (!num1.equals(num2)) {
            if (debug) {
              System.out.printf("*****num1 != num2 for %s%n", line);
            }
            continue;
          }
          int code = Integer.parseInt(num1);

          EcmwfEntry entry = new EcmwfEntry(code, desc);
          builder.put(entry.getCode(), entry);
          if (debug) {
            System.out.printf(" %s%n", entry);
          }
        }
      }
    }
    return builder.build();
  }

  private class EcmwfEntry implements Entry, Comparable<Entry> {
    private final int codeValue;
    private final String name;

    public EcmwfEntry(int codeValue, String name) {
      this.codeValue = codeValue;
      this.name = name;
    }

    @Override
    public int getCode() {
      return codeValue;
    }

    @Override
    public String getName() {
      return name;
    }

    @Override
    public int compareTo(Entry o) {
      return codeValue - o.getCode();
    }

    @Override
    public String toString() {
      return MoreObjects.toStringHelper(this)
          .add("codeValue", codeValue)
          .add("name", name)
          .toString();
    }
  }

}