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

package ucar.nc2.units;

import ucar.units.ConversionException;

import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.Duration;

import java.io.IOException;
import java.util.Calendar;
import java.util.Date;

/**
 * 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
 *

 *

 *
 * @author john caron
 * @see "http://www.unidata.ucar.edu/projects/THREDDS/tech/catalog/InvCatalogSpec.html#durationType"
 */

public class TimeDuration {
  static private final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TimeDuration.class);

  /* private static TimeUnit secUnits;

  static {
    try {
      secUnits = new TimeUnit("1 sec");
    } catch (Exception e) {
      e.printStackTrace();
    }
  } */

  private String text;
  private TimeUnit timeUnit;
  private boolean isBlank;

  static private boolean debug = false;

  private TimeDuration() {
  }

  /**
   * Copy constructor.
   *
   * @param src copy this
   */
  public TimeDuration(TimeDuration src) {
    text = src.getText();
    timeUnit = new TimeUnit(src.getTimeUnit());
    isBlank = src.isBlank();
  }

  /**
   * Construct from a TimeUnit.
   *
   * @param timeUnit copy this
   */
  public TimeDuration(TimeUnit timeUnit) {
    this.timeUnit = timeUnit;
    this.text = timeUnit.toString();
  }

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

    // see if its blank   LOOK WTF ?
    if (text.length() == 0) {
      isBlank = true;
      try {
        timeUnit = new TimeUnit("1 sec");
      } catch (Exception e) { // cant happen
      }
      return;
    }

    // see if its a udunits string
    try {
      timeUnit = new TimeUnit(text);
      if (debug) System.out.println(" set time unit= " + timeUnit);

    } catch (Exception e) {
      // see if its a xsd:duration
      try {
        DatatypeFactory factory = DatatypeFactory.newInstance();
        Duration d = factory.newDuration(text);
        //long secs = d.getTimeInMillis(new Date()) / 1000;
        Calendar c  = Calendar.getInstance();
        c.set(1900, 0, 1, 0, 0, 0);
        long secs =d.getTimeInMillis( c.getTime() ) / 1000;
        timeUnit = new TimeUnit(secs + " secs");

      } catch (Exception e1) {
        throw new java.text.ParseException(e.getMessage(), 0);
      }

    }

  }

  /**
   * 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."
   *
   * @param text parse this text, format PnYnMnDTnHnMnS
   * @return TimeDuration
   * @throws java.text.ParseException when text is misformed
   */
  static public TimeDuration parseW3CDuration(String text) throws java.text.ParseException {
    TimeDuration td = new TimeDuration();

    text = (text == null) ? "" : text.trim();
    td.text = text;

    try {
      DatatypeFactory factory = DatatypeFactory.newInstance();
      Duration d = factory.newDuration(text);
      //long secs = d.getTimeInMillis(new Date()) / 1000;

      Calendar c  = Calendar.getInstance();
      c.set(1900, 0, 1, 0, 0, 0);
      long secs =d.getTimeInMillis( c.getTime() ) / 1000;

      td.timeUnit = new TimeUnit(secs + " secs");
    } catch (Exception e) {
      throw new java.text.ParseException(e.getMessage(), 0);
    }
    return td;
  }

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

  /**
   * Get the time duration in a specified unit of time.
   *
   * @param want in these units
   * @return the duration in units
   * @throws ucar.units.ConversionException is specified unit is not compatible with time
   */
  public double getValue(TimeUnit want) throws ConversionException {
    return timeUnit.convertTo(timeUnit.getValue(), want);
  }

  public double getValue(String unit) {
    try {
      TimeUnit tdayUnit = new TimeUnit(unit);
      return getValue(tdayUnit);

    } catch (Exception e) {
      log.error("Illegal time unit =", unit, e);
      return -1;
    }
  }

  /**
   * Get the duration in seconds
   * @return the duration in seconds
   */
  public double getValueInSeconds() {
    return timeUnit.getValueInSeconds();
  }

  /**
   * Set the duration in seconds
   * @param secs the duration in seconds
   */
  public void setValueInSeconds(double secs) {
    timeUnit.setValueInSeconds(secs);
    text = null;
  }

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

  /**
   * Get the corresponding time unit
   * @return the corresponding time unit
   */
  public TimeUnit getTimeUnit() {
    return timeUnit;
  }

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

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

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

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

    TimeDuration to = (TimeDuration) o;
    return to.getValueInSeconds() == getValueInSeconds();
  }

  public Date add( Date d) {
    return timeUnit.add(d);
  }

  ////////////////////////////////////////////
  // test

  private static void doDuration(String s) {
    try {
      System.out.println("start = (" + s + ")");
      TimeDuration d = new TimeDuration(s);
      System.out.println("duration = (" + d.toString() + ")");
    }
    catch (java.text.ParseException e) {
      e.printStackTrace();
    }
  }

  public static void main2(String[] args) {
    doDuration("3 days");
  }

  public static void main(String arg[]) throws Exception {

    DatatypeFactory factory = DatatypeFactory.newInstance();

    Duration d = factory.newDuration("P3D");
    long secs1 = d.getTimeInMillis(new Date()) / 1000;
    Calendar c = Calendar.getInstance();
    c.set(1970, 0, 1, 0, 0, 0);
    long secs2 = d.getTimeInMillis(c.getTime()) / 1000;

    System.out.printf("%d %d same = %s%n", secs1, secs2, secs1 == secs2);
  }
}


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

package ucar.nc2.stream;

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

import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;

/**
 * Write a NetcdfFile to a WritableByteChannel using ncstream protocol.
 * Experimental, checking performance vs OutputStream
 *
 * @author caron
 * @since 7/10/12
 */
public class NcStreamWriterChannel {
  static private final long maxChunk = 1000 * 1000; // 1 MByte
  static private final int sizeToCache = 100; // when to store a variable's data in the header, ie "immediate" mode

  private NetcdfFile ncfile;
  private NcStreamProto.Header header;
  private boolean show = false;

  public NcStreamWriterChannel(NetcdfFile ncfile, String location) throws IOException {
    this.ncfile = ncfile;
    NcStreamProto.Group.Builder rootBuilder = NcStream.encodeGroup(ncfile.getRootGroup(), sizeToCache);

    NcStreamProto.Header.Builder headerBuilder = NcStreamProto.Header.newBuilder();
    headerBuilder.setLocation(location == null ? ncfile.getLocation() : location);
    if (ncfile.getTitle() != null) headerBuilder.setTitle(ncfile.getTitle());
    if (ncfile.getId() != null) headerBuilder.setId(ncfile.getId());
    headerBuilder.setRoot(rootBuilder);

    header = headerBuilder.build();
  }

  public long sendStart(WritableByteChannel wbc) throws IOException {
    return writeBytes(wbc, NcStream.MAGIC_START);
  }

  public long sendEnd(WritableByteChannel wbc) throws IOException {
    return writeBytes(wbc, NcStream.MAGIC_END);
  }

  public long sendHeader(WritableByteChannel wbc) throws IOException {
    long size = 0;

    //// header message
    size += writeBytes(wbc, NcStream.MAGIC_HEADER);
    byte[] b = header.toByteArray();
    size += NcStream.writeVInt(wbc, b.length); // len
    if (show) System.out.println("Write Header len=" + b.length);

    // payload
    size += writeBytes(wbc, b);
    if (show) System.out.println(" header size=" + size);

    return size;
  }

  public long sendData(Variable v, Section section, WritableByteChannel wbc, NcStreamCompression compress) throws IOException, InvalidRangeException {
    if (show) System.out.printf(" %s section=%s%n", v.getFullName(), section);

    ByteOrder bo = ByteOrder.nativeOrder(); // reader makes right
    long size = 0;
    size += writeBytes(wbc, NcStream.MAGIC_DATA); // magic
    NcStreamProto.Data dataProto = NcStream.encodeDataProto(v, section, compress.type, bo, 0);
    byte[] datab = dataProto.toByteArray();
    size += NcStream.writeVInt(wbc, datab.length); // dataProto len
    size += writeBytes(wbc, datab); // dataProto

    // version < 3
    if (v.getDataType() == DataType.SEQUENCE) {
      assert (v instanceof Structure);
      int count = 0;
      DataOutputStream os = new DataOutputStream(Channels.newOutputStream(wbc));
      Structure seq = (Structure) v; // superclass for Sequence, SequenceDS
      //coverity[FB.BC_UNCONFIRMED_CAST]
      try (StructureDataIterator iter = seq.getStructureIterator(-1)) {
        while (iter.hasNext()) {
          size += writeBytes(wbc, NcStream.MAGIC_VDATA); // magic
          ArrayStructureBB abb = StructureDataDeep.copyToArrayBB(iter.next());
          size += NcStream.encodeArrayStructure(abb, bo, os);
          count++;
        }
      }
      size += writeBytes(wbc, NcStream.MAGIC_VEND);
      if (show) System.out.printf(" NcStreamWriter sent %d sdata bytes = %d%n", count, size);
      return size;
    }

    // regular arrays
    long len = section.computeSize();
    if ((v.getDataType() != DataType.STRING) && (v.getDataType() != DataType.OPAQUE) && !v.isVariableLength())
      len *= v.getElementSize(); // nelems for vdata, else nbytes

    size += NcStream.writeVInt(wbc, (int) len); // data len or number of objects
    if (show) System.out.printf("  %s proto=%d data=%d%n", v.getFullName(), datab.length, len);

    size += v.readToByteChannel(section, wbc); // try to do a direct transfer

    return size;
  }

  /* public long sendData(WritableByteChannel wbc, StructureData sdata) throws IOException {
   long size = 0;
   ByteBuffer bb = IospHelper.copyToByteBuffer(sdata);
   byte[] datab = bb.array();
   size += writeBytes(wbc, NcStream.MAGIC_DATA); // magic
   size += NcStream.writeVInt(wbc, datab.length); // data len
   size += writeBytes(wbc, datab); // data
   return size;
 } */

  private int writeBytes(WritableByteChannel wbc, byte[] b) throws IOException {
    return wbc.write(ByteBuffer.wrap(b));
  }

  public long streamAll(WritableByteChannel wbc) throws IOException, InvalidRangeException {
    long size = writeBytes(wbc, NcStream.MAGIC_START);
    size += sendHeader(wbc);
    if (show) System.out.printf(" data starts at= %d%n", size);

    for (Variable v : ncfile.getVariables()) {
      NcStreamCompression compress;
      Attribute compressAtt = v.findAttribute(CDM.COMPRESS);
      if (compressAtt != null && compressAtt.isString()) {
        String compType = compressAtt.getStringValue();
        if (compType.equalsIgnoreCase(CDM.COMPRESS_DEFLATE)) {
          compress = NcStreamCompression.deflate();
        } else {
          if (show) System.out.printf(" Unknown compression type %s. Defaulting to none.%n", compType);
          compress = NcStreamCompression.none();
        }
      } else {
        compress = NcStreamCompression.none();
      }

      long vsize = v.getSize() * v.getElementSize();
      //if (vsize < sizeToCache) continue; // in the header;
      if (show) System.out.printf(" var %s len=%d starts at= %d%n", v.getFullName(), vsize, size);

      if (vsize > maxChunk) {
        size += copyChunks(wbc, v, maxChunk, compress);
      } else {
        size += sendData(v, v.getShapeAsSection(), wbc, compress);
      }
    }

    size += writeBytes(wbc, NcStream.MAGIC_END);
    if (show) System.out.printf("total size= %d%n", size);
    return size;
  }

  private long copyChunks(WritableByteChannel wbc, Variable oldVar, long maxChunkSize, NcStreamCompression compress) throws IOException {
    long maxChunkElems = maxChunkSize / oldVar.getElementSize();
    FileWriter2.ChunkingIndex index = new FileWriter2.ChunkingIndex(oldVar.getShape());
    long size = 0;
    while (index.currentElement() < index.getSize()) {
      try {
        int[] chunkOrigin = index.getCurrentCounter();
        int[] chunkShape = index.computeChunkShape(maxChunkElems);
        size += sendData(oldVar, new Section(chunkOrigin, chunkShape), wbc, compress);
        index.setCurrentCounter(index.currentElement() + (int) Index.computeSize(chunkShape));

      } catch (InvalidRangeException e) {
        e.printStackTrace();
        throw new IOException(e.getMessage());
      }
    }
    return size;
  }

  static public void main2(String args[]) throws InvalidRangeException {
    int[] totalShape = new int[]{1, 40, 530, 240};
    int[] chunkShape = new int[]{1, 1, 530, 240};
    FileWriter2.ChunkingIndex index = new FileWriter2.ChunkingIndex(totalShape);
    while (index.currentElement() < index.getSize()) {
      int[] chunkOrigin = index.getCurrentCounter();
      System.out.printf(" %s%n", new Section(chunkOrigin, chunkShape));
      index.setCurrentCounter(index.currentElement() + (int) Index.computeSize(chunkShape));
    }

  }

  static public void main(String args[]) throws InvalidRangeException {
    long maxChunkElems = maxChunk / 4;
    int[] totalShape = new int[]{1, 40, 530, 240};
    //int[] chunkShape = new int[] {1, 1, 530, 240};
    FileWriter2.ChunkingIndex index = new FileWriter2.ChunkingIndex(totalShape);
    while (index.currentElement() < index.getSize()) {
      int[] chunkOrigin = index.getCurrentCounter();
      int[] chunkShape = index.computeChunkShape(maxChunkElems);
      System.out.printf(" %s%n", new Section(chunkOrigin, chunkShape));
      index.setCurrentCounter(index.currentElement() + (int) Index.computeSize(chunkShape));
    }

  }

}


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

package ucar.nc2.grib.grib1.tables;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.Namespace;
import org.jdom2.input.SAXBuilder;
import thredds.client.catalog.Catalog;
import ucar.nc2.grib.GribResourceReader;
import ucar.nc2.grib.grib1.*;
import ucar.unidata.util.StringUtil2;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * A Grib1 Parameter Table (table 2). This is a map: code -> Grib1Parameter. Handles reading the
 * table in from various formats
 *
 * @author caron
 * @since 11/16/11
 */
public class Grib1ParamTableReader {
  private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(Grib1ParamTableReader.class);
  private static final boolean debug = false;

  private int center_id;
  private int subcenter_id;
  private int version;
  private final String name;  // name of the table
  private String path; // path of filename containing this table
  private String desc; // optional desc from within the file
  private Map<Integer, Grib1Parameter> parameters; // param number -> param
  private boolean useName;

  /**
   * Read a dataset-specific table from a file
   *
   * @param path read from this path, may be reletive
   */
  public Grib1ParamTableReader(String path) {
    this.path = StringUtil2.replace(path, "\\", "/");
    File f = new File(path);
    this.name = f.getName();
    this.parameters = readParameterTable();
  }

  /**
   * Create a standard table from a file. Defer reading until getParameters is called.
   *
   * @param center_id associate with this center
   * @param subcenter_id associate with this subcenter
   * @param version associate with this version
   * @param path read from this path, may be reletive
   */
  Grib1ParamTableReader(int center_id, int subcenter_id, int version, String path) {
    this.center_id = center_id;
    this.subcenter_id = subcenter_id;
    this.version = version;
    this.path = StringUtil2.replace(path, "\\", "/");
    File f = new File(path);
    this.name = f.getName();
  }

  /**
   * Create a dataset-specific table from a jdom tree using the DSS parser
   *
   * @param paramTableElem the jdom tree
   */
  Grib1ParamTableReader(org.jdom2.Element paramTableElem) {
    this.name = paramTableElem.getChildText("title");
    DssParser p = new DssParser(Catalog.ncmlNS);
    this.parameters = p.parseXml(paramTableElem);
  }

  public int getCenter_id() {
    return center_id;
  }

  public int getSubcenter_id() {
    return subcenter_id;
  }

  public int getVersion() {
    return version;
  }

  public String getName() {
    return name;
  }

  public String getDesc() {
    return desc;
  }

  // whether to use the param name or description in the variable name
  public boolean useParamName() {
    return useName;
  }

  public int getKey() {
    return Grib1ParamTables.makeKey(center_id, subcenter_id, version);
  }

  public String getPath() {
    return path;
  }

  public Map<Integer, Grib1Parameter> getParameters() {
    if (parameters == null) {
      parameters = readParameterTable();
    }
    return parameters;
  }

  public void setParameters(Map<Integer, Grib1Parameter> params) {
    parameters = params;
  }

  /**
   * Get the parameter with id. If not found, look in default table.
   *
   * @param id the parameter number
   * @return the Grib1Parameter, or null if not found
   */
  public Grib1Parameter getParameter(int id) {
    if (parameters == null) {
      parameters = readParameterTable();
    }

    return parameters.get(id);
  }

  /**
   * Get the parameter with id, but dont look in default table.
   *
   * @param id the parameter number
   * @return the Grib1Parameter, or null if not found
   */
  public Grib1Parameter getLocalParameter(int id) {
    if (parameters == null) {
      parameters = readParameterTable();
    }
    return parameters.get(id);
  }

  @Override
  public String toString() {
    return "Grib1ParamTable{" +
        "center_id=" + center_id +
        ", subcenter_id=" + subcenter_id +
        ", version=" + version +
        ", name='" + name + '\'' +
        ", path='" + path + '\'' +
        '}';
  }

  //////////////////////////////////////////////////////////////////////////////////////
  // reading

  private synchronized Map<Integer, Grib1Parameter> readParameterTable() {
    if (path == null) {
      throw new IllegalStateException(name);
    }

    try {
      if (name.startsWith("table_2_") || name.startsWith("local_table_2_")) {
        return readParameterTableEcmwf(); // ecmwf
      } else if (name.startsWith("US058")) {
        return readParameterTableXml(new FnmocParser());// FNMOC
      } else if (name.endsWith(".tab")) {
        return readParameterTableTab();                               // wgrib format
      } else if (name.endsWith(".wrf")) {
        return readParameterTableSplit("\\|", new int[]{0, 3, 1, 2}); // WRF AMPS
      } else if (name.endsWith(".h")) {
        return readParameterTableNcl(); // NCL
      } else if (name.endsWith(".dss")) {
        return readParameterTableSplit("\t", new int[]{0, -1, 1, 2}); // NCAR DSS
      } else if (name.endsWith(".xml")) {
        return readParameterTableXml(new DssParser(Namespace.NO_NAMESPACE));// NCAR DSS XML format
      } else if (name.startsWith("2.98")) {
        return readParameterTableEcmwfEcCodes(); // ecmwf from ecCodes package
      } else {
        throw new RuntimeException(
            "Grib1ParamTableReader: Dont know how to read " + name + " file=" + path);
      }

    } catch (IOException ioError) {
      logger.warn(
          "An error occurred in Grib1ParamTable while trying to open the parameter table {}:{}",
          path, ioError.getMessage());
      throw new RuntimeException(ioError);
    }
  }

/*
 * Brazilian Space Agency - INPE/CPTEC
 * Center: 46
 * Subcenter: 0
 * Parameter table version: 254
 *

TBLE2 cptec_254_params[] = {
{1, "Pressure", "hPa", "PRES"},
{2, "Pressure reduced to MSL", "hPa", "PSNM"},
{3, "Pressure tendency", "Pa/s", "TSPS"},
{6, "Geopotential", "dam", "GEOP"},
{7, "Geopotential height", "gpm", "ZGEO"},
{8, "Geometric height", "m", "GZGE"},
{11, "ABSOLUTE TEMPERATURE", "K", "TEMP"},

   */

  private static final Pattern nclPattern = Pattern.compile("\\{(\\d*),\\s*\"([^\"]*)\",\\s*\"([^\"]*)\",\\s*\"([^\"]*)\".*");

  private Map<Integer, Grib1Parameter> readParameterTableNcl() throws IOException {
    HashMap<Integer, Grib1Parameter> result = new HashMap<>();

    try (InputStream is = GribResourceReader.getInputStream(path);
        BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {

      // Ignore header
      while (true) {
        String line = br.readLine();
        if (line == null) {
          break; // done with the file
        }
        if (line.startsWith("TBLE2")) {
          break;
        }

        if (line.contains("Center:")) {
          center_id = extract(line, "Center:");
        } else if (line.contains("Subcenter:")) {
          subcenter_id = extract(line, "Subcenter:");
        } else if (line.contains("version:")) {
          version = extract(line, "version:");
        }
      }

      while (true) {
        String line = br.readLine();
        if (line == null) {
          break; // done with the file
        }
        if ((line.length() == 0) || line.startsWith("#")) {
          continue;
        }

        Matcher m = nclPattern.matcher(line);
        if (!m.matches()) {
          continue;
        }

        int p1;
        try {
          p1 = Integer.parseInt(m.group(1));
        } catch (Exception e) {
          logger.warn("Cant parse " + m.group(1) + " in file " + path);
          continue;
        }
        Grib1Parameter parameter = new Grib1Parameter(this, p1, m.group(4), m.group(2),
            m.group(3));
        result.put(parameter.getNumber(), parameter);
        logger.debug(" %s%n", parameter);
      }

      return Collections.unmodifiableMap(result);  // all at once - thread safe
    }

  }

  private int extract(String line, String key) {
    int pos = line.indexOf(key);
    if (pos < 0) {
      return -1;
    }
    String want = line.substring(pos + key.length());
    try {
      return Integer.parseInt(want.trim());
    } catch (NumberFormatException e) {
      logger.warn("BAD %s (%s)%n", line, path);
      return -1;
    }
  }

  /*
  WMO standard table 2: Version Number 3.
  Codes and data units for FM 92-X Ext.GRIB.
  ......................
  001
  P
  Pressure
  Pa
  Pa
  ......................
  002
  MSL
  Mean sea level pressure
  Pa
  Pa
  ......................
  003
  None
  Pressure tendency
  Pa s**-1
  Pa s**-1
  ......................
  004
  PV
  Potential vorticity
  K m**2 kg**-1 s**-1
  K m**2 kg**-1 s**-1
  ......................
  005
  None
  ICAO Standard Atmosphere reference height
  m
  m
   */

  private Map<Integer, Grib1Parameter> readParameterTableEcmwf() throws IOException {
    HashMap<Integer, Grib1Parameter> result = new HashMap<>();

    try (InputStream is = GribResourceReader.getInputStream(path);
        BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
      String line = br.readLine();
      if (line == null) {
        throw new FileNotFoundException(path + " is empty");
      }
      if (!line.startsWith("...")) {
        this.desc = line; // maybe ??
      }

      while (line != null && !line.startsWith("...")) {
        line = br.readLine(); // skip
      }

      while (true) {
        line = br.readLine();
        if (line == null) {
          break; // done with the file
        }
        if ((line.length() == 0) || line.startsWith("#")) {
          continue;
        }
        if (line.startsWith("...")) { // ...  may have already been read
          line = br.readLine();
          if (line == null) {
            break;
          }
        }
        String num = line.trim();
        line = br.readLine();
        String name = (line != null) ? line.trim() : null;
        line = br.readLine();
        String desc = (line != null) ? line.trim() : null;
        line = br.readLine();
        String units1 = (line != null) ? line.trim() : null;

        // optional notes
        line = br.readLine();
        String notes = (line == null || line.startsWith("...")) ? null : line.trim();
        if (desc != null && desc.equalsIgnoreCase("undefined")) {
          continue; // skip
        }

        int p1;
        try {
          p1 = Integer.parseInt(num);
        } catch (Exception e) {
          logger.warn("Cant parse " + num + " in file " + path);
          continue;
        }
        Grib1Parameter parameter = new Grib1Parameter(this, p1, name, desc, units1);
        result.put(parameter.getNumber(), parameter);
        logger.debug(" %s (%s)%n", parameter, notes);
      }
      return Collections.unmodifiableMap(result);  // all at once - thread safe
    }

  }

  /**
   * This method will read in ECMWF grib1 tables. Note that these tables are generated locally by
   * Unidata, and come directly from the ECMWF GRIB-API package localConcepts files. They are
   * generated by:
   *

   * ucar.nc2.grib.grib1.tables.EcmwfLocalConcepts
   *

   * The original localConcepts files are located in:
   *

   * grib/src/main/sources/ecmwfEcCodes/
   *

   * Since we write the table file that are ultimately read by CDM, the format is controled and is
   * the following:
   *

   * paramNum shortName [description] (units)
   *

   * for example,
   *

   * 251 atte [Adiabatic tendency of temperature] (K)
   */
  private Map<Integer, Grib1Parameter> readParameterTableEcmwfEcCodes() throws IOException {
    HashMap<Integer, Grib1Parameter> result = new HashMap<>();

    try (InputStream is = GribResourceReader.getInputStream(path);
      BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
      String line = br.readLine();
      if (line == null) {
        throw new FileNotFoundException(path + " is empty");
      }

      // create table name from file name
      String[] splitPath = path.split("/");
      String tableNum = splitPath[splitPath.length - 1].replace(".table", "");
      this.desc = "ECMWF GRIB API TABLE " + tableNum;

      // skip header
      while (line != null && !line.startsWith("#")) {
        line = br.readLine(); // skip
      }

      // keep going until the end of the file is reached (line == null)
      while (true) {
        // exmaple: 251 atte [Adiabatic tendency of temperature] (K)
        line = br.readLine();
        if (line == null) {
          break; // done with the file
        }
        if ((line.length() == 0) || line.startsWith("#")) {
          continue;
        }

        // get unit - (K)
        String[] tmpUnitArray = line.split("\\(");
        String tmpUnit = tmpUnitArray[tmpUnitArray.length - 1];
        int lastUnitIndex;
        while ((lastUnitIndex = tmpUnit.lastIndexOf(")")) > 0) {
          tmpUnit = tmpUnit.substring(0, lastUnitIndex).trim();
        }
        String unit = tmpUnit.trim();
        // unit = Util.cleanUnit(unit); // fixes some common unit mistakes  // jcaron - just use unit as it is

        // get parameter number - 251
        String[] lineArray = line.trim().split("\\s+"); // all and any white space
        String num = lineArray[0];

        // get shortName - atte
        String name = lineArray[1].trim();
        //if (name.equals("~")) {}; - todo create name from long name(?)

        // get description. bracketed by [] - [Adiabatic Tendency of temperature]
        int startDesc = line.indexOf("[");
        int endDesc = line.indexOf("]");
        String desc = line.substring(startDesc, endDesc).trim();

        // stuff information into a Grib1Parameter object
        int p1;
        try {
          p1 = Integer.parseInt(num);
        } catch (Exception e) {
          logger.warn("Cant parse " + num + " in file " + path);
          continue;
        }
        Grib1Parameter parameter = new Grib1Parameter(this, p1, name, desc, unit);
        result.put(parameter.getNumber(), parameter);
        logger.debug(" %s%n", parameter);
      }

      return Collections.unmodifiableMap(result);  // all at once - thread safe
    }
  }

  private Map<Integer, Grib1Parameter> readParameterTableXml(XmlTableParser parser) throws IOException {
    try (InputStream is = GribResourceReader.getInputStream(path)) {
      SAXBuilder builder = new SAXBuilder();
      org.jdom2.Document doc = builder.build(is);
      Element root = doc.getRootElement();
      return parser.parseXml(root);  // all at once - thread safe

    } catch (JDOMException | IOException e) {
      throw new IOException(e);
    }
  }

  private interface XmlTableParser {
    Map<Integer, Grib1Parameter> parseXml(Element root);
  }

  private class DssParser implements XmlTableParser {

    private final Namespace ns;

    DssParser(Namespace ns) {
      this.ns = ns;
    }

    /* http://dss.ucar.edu/metadata/ParameterTables/WMO_GRIB1.60-1.3.xml
     <parameter code="5">
     <description>ICAO Standard Atmosphere reference height</description>
     <units>m</units>
     </parameter>
     */
    public Map<Integer, Grib1Parameter> parseXml(Element root) {
      Map<Integer, Grib1Parameter> result = new HashMap<>();
      List<Element> params = root.getChildren("parameter", ns);
      for (Element elem1 : params) {
        int code = Integer.parseInt(elem1.getAttributeValue("code"));
        String desc = elem1.getChildText("description", ns);
        if (desc == null) {
          continue;
        }
        String units = elem1.getChildText("units", ns);
        if (units == null) {
          units = "";
        }
        String name = elem1.getChildText("name", ns);
        if (name == null) {
          name = elem1.getChildText("shortName", ns);
        }
        String cf = elem1.getChildText("CF", ns);
        Grib1Parameter parameter = new Grib1Parameter(Grib1ParamTableReader.this, code, name, desc,
            units, cf);
        result.put(parameter.getNumber(), parameter);
        logger.debug(" %s%n", parameter);
      }
      return Collections.unmodifiableMap(result);  // all at once - thread safe
    }
  }

  private class FnmocParser implements XmlTableParser {
    /*
    <pnTable>
      <name>Master Parameter Table</name>
      <updatedDTG>201110110856</updatedDTG>
      <tableStatus setDTG="2007050712">current</tableStatus>
      <fnmocTable>
        <entry>
          <grib1Id>001</grib1Id>
          <fnmocId>pres</fnmocId>
          <name>pres</name>
          <nameFull/>
          <description>Commonly used for atmospheric pressure, the pressure exerted by the atmosphere as a consequence of
            gravitational attraction exerted upon the "column" of air lying directly above the point in question.
          </description>
          <unitsFNMOC>pa</unitsFNMOC>
          <productionStatus>current</productionStatus>
        </entry>
    */

    public HashMap<Integer, Grib1Parameter> parseXml(Element root) {
      HashMap<Integer, Grib1Parameter> result = new HashMap<>();
      Element fnmocTable = root.getChild("fnmocTable");
      List<Element> params = fnmocTable.getChildren("entry");
      for (Element elem1 : params) {
        int code;
        try {
          code = Integer.parseInt(elem1.getChildText("grib1Id"));
        } catch (NumberFormatException e) {
          logger.warn("BAD number= %s%n", elem1.getChildText("grib1Id"));
          continue;
        }
        String desc = elem1.getChildText("description");
        if (desc == null) {
          continue;
        }
        //if (desc.startsWith("no definition")) continue; // skip; use standard def
        desc = StringUtil2.collapseWhitespace(desc);
        String units = elem1.getChildText("unitsFNMOC");
        if (units == null) {
          units = "";
        }
        String name = elem1.getChildText("name");
        Grib1Parameter parameter = new Grib1Parameter(Grib1ParamTableReader.this, code, name, desc,
            units, null);
        result.put(parameter.getNumber(), parameter);
        logger.debug(" %s%n", parameter);
      }
      useName = true;
      return result;
    }
  }

  // order: num, name, desc, unit
  private Map<Integer, Grib1Parameter> readParameterTableSplit(String regexp, int[] order)
      throws IOException {
    HashMap<Integer, Grib1Parameter> result = new HashMap<>();

    try (InputStream is = GribResourceReader.getInputStream(path);
        BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {

      // rdg - added the 0 line length check to cover the case of blank lines at the end of the parameter table file.
      while (true) {
        String line = br.readLine();
        if (line == null) {
          break;
        }
        if ((line.length() == 0) || line.startsWith("#")) {
          continue;
        }
        String[] flds = line.split(regexp);

        int p1 = Integer.parseInt(flds[order[0]].trim()); // must have a number
        String name = (order[1] >= 0) ? flds[order[1]].trim() : null;
        String desc = flds[order[2]].trim();
        String units = (flds.length > order[3]) ? flds[order[3]].trim() : "";

        Grib1Parameter parameter = new Grib1Parameter(this, p1, name, desc, units);
        result.put(parameter.getNumber(), parameter);
        logger.debug(" %s%n", parameter);
      }
      return Collections.unmodifiableMap(result);  // all at once - thread safe
    }

  }

  private Map<Integer, Grib1Parameter> readParameterTableTab() throws IOException {

    try (InputStream is = GribResourceReader.getInputStream(path)) {
      HashMap<Integer, Grib1Parameter> params;
      try (BufferedReader br = new BufferedReader(
          new InputStreamReader(is, StandardCharsets.UTF_8))) {
        br.readLine(); // skip a line

        // thread safe - local var
        params = new HashMap<>();
        while (true) {
          String line = br.readLine();
          if (line == null) {
            break;
          }
          if ((line.length() == 0) || line.startsWith("#")) {
            continue;
          }
          String[] tableDefArr = line.split(":");

          int p1 = Integer.parseInt(tableDefArr[0].trim());
          String name = tableDefArr[1].trim();
          String desc, units;
          // check to see if unit defined, if not, parameter is undefined
          if (tableDefArr[2].indexOf('[') == -1) {
            // Undefined unit
            desc = tableDefArr[2].trim();
            units = "";
          } else {
            String[] arr2 = tableDefArr[2].split("\\[");
            desc = arr2[0].trim();
            units = arr2[1].substring(0, arr2[1].lastIndexOf(']')).trim();
          }

          Grib1Parameter parameter = new Grib1Parameter(this, p1, name, desc, units);
          if (!parameter.getDescription().equalsIgnoreCase("undefined")) {
            params.put(parameter.getNumber(), parameter);
          }
          logger.debug(parameter.getNumber() + " " + parameter.getDescription() + " " + parameter.getUnit());
        }
      }
      return Collections.unmodifiableMap(params);  // all at once - thread safe
    }

  }
}


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

import java.io.Serializable;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;

/**
 * Provides a concrete implementation of a database of unit prefixes.
 *
 * Instances of this class are modifiable.
 *
 * @author Steven R. Emmerson
 */
public class PrefixDBImpl implements PrefixDB, Serializable {
	private static final long			serialVersionUID	= 1L;

	/**
	 * The set of prefix names.
	 *
	 * @serial
	 */
	private final SortedSet<Prefix>		nameSet;

	/**
	 * The set of prefix symbols.
	 *
	 * @serial
	 */
	private final SortedSet<Prefix>		symbolSet;

	/**
	 * The mapping between prefix values and prefixes.
	 *
	 * @serial
	 */
	private final Map<Double, Prefix>	valueMap;

	/**
	 * Constructs from nothing.
	 */
	public PrefixDBImpl() {
		nameSet = new TreeSet<Prefix>();
		symbolSet = new TreeSet<Prefix>();
		valueMap = new TreeMap<Double, Prefix>();
	}

	/**
	 * Adds a prefix to the database by name.
	 *
	 * @param name
	 *            The name of the prefix to be added.
	 * @param value
	 *            The value of the prefix.
	 * @throws PrefixExistsException
	 *             Another prefix with the same name or value already exists in
	 *             the database.
	 */
	public void addName(final String name, final double value)
			throws PrefixExistsException {
		final Prefix prefix = new PrefixName(name, value);
		nameSet.add(prefix);
	}

	/**
	 * Adds a prefix symbol to the database.
	 *
	 * @param symbol
	 *            The symbol of the prefix to be added.
	 * @param value
	 *            The value of the prefix.
	 * @throws PrefixExistsException
	 *             Another prefix with the same symbol or value already exists
	 *             in the database.
	 */
	public void addSymbol(final String symbol, final double value)
			throws PrefixExistsException {
		final Prefix prefix = new PrefixSymbol(symbol, value);
		symbolSet.add(prefix);
		valueMap.put(new Double(value), prefix);
	}

	/**
	 * Gets a prefix by name.
	 *
	 * @param string
	 *            The name to be matched.
	 * @return The prefix whose name matches or null.
	 */
	public Prefix getPrefixByName(final String string) {
		return getPrefix(string, nameSet);
	}

	/**
	 * Gets a prefix by symbol.
	 *
	 * @param string
	 *            The symbol to be matched.
	 * @return The prefix whose symbol matches or null.
	 */
	public Prefix getPrefixBySymbol(final String string) {
		return getPrefix(string, symbolSet);
	}

	/**
	 * Gets a prefix by value.
	 *
	 * @param value
	 *            The value to be matched.
	 * @return The prefix whose value matches or null.
	 */
	public Prefix getPrefixByValue(final double value) {
		return valueMap.get(new Double(value));
	}

	/**
	 * Returns the prefix from the given set with the given identifier.
	 *
	 * @param string
	 *            The prefix identifier.
	 * @param set
	 *            The set to search.
	 */
	private static Prefix getPrefix(final String string, final Set<Prefix> set) {
		for (final Iterator<Prefix> iter = set.iterator(); iter.hasNext();) {
			final Prefix prefix = iter.next();
			final int comp = prefix.compareTo(string);
			if (comp == 0) {
				return prefix;
			}
			if (comp > 0) {
				break;
			}
		}
		return null;
	}

	/**
	 * Returns a string representation of this database.
	 *
	 * @return A string representation of this database.
	 */
	@Override
	public String toString() {
		return "nameSet=" + nameSet + "symbolSet=" + symbolSet + "valueMap="
				+ valueMap;
	}

	/**
	 * Gets an iterator over the prefixes in the database.
	 *
	 * @return An iterator over the entries in the database. The objects
	 *         returned by the iterator will be of type Prefix.
	 */
	@SuppressWarnings("unchecked")
	public Iterator iterator() {
		return nameSet.iterator();
	}

}


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

// $Id: ADNWriter.java 48 2006-07-12 16:15:40Z caron $

package thredds.catalog.dl;

import thredds.catalog.*;
import thredds.catalog.crawl.CatalogCrawler;
import ucar.nc2.constants.CDM;
import ucar.nc2.units.DateRange;
import ucar.nc2.units.DateType;
import ucar.nc2.units.TimeDuration;

import org.jdom2.*;
import org.jdom2.output.*;

import java.io.*;
import java.util.*;
import java.net.URI;

import ucar.unidata.geoloc.LatLonPointImpl;
import ucar.nc2.constants.FeatureType;
import ucar.unidata.util.StringUtil2;

public class ADNWriter {
  private static final Namespace defNS = Namespace.getNamespace("http://adn.dlese.org");
  private static final String schemaLocation = "http://www.dlese.org/Metadata/adn-item/0.6.50/record.xsd";
  // private static final String schemaLocationLocal = defNS.getURI() + " file://c:/dev/metadata/adn/0.6.50/record.xsd";

  private String fileDir;
  private StringBuilder messBuffer;

  public ADNWriter( ) {
  }

  public void writeDatasetEntries( InvCatalogImpl cat, String fileDir, StringBuilder mess) {
    this.fileDir = fileDir;
    this.messBuffer = mess;

    File dir = new File(fileDir);
    if (!dir.exists()) {
      boolean ret = dir.mkdirs();
      assert ret;
    }

    CatalogCrawler.Listener listener = new CatalogCrawler.Listener() {
      public void getDataset(InvDataset ds, Object context) {
        doOneDataset(ds);
      }
      public boolean getCatalogRef(InvCatalogRef dd, Object context) { return true; }

    };

    ByteArrayOutputStream bis = new ByteArrayOutputStream();
    PrintWriter pw = new PrintWriter( new OutputStreamWriter(bis, CDM.utf8Charset));
    CatalogCrawler crawler = new CatalogCrawler( CatalogCrawler.USE_ALL, true, listener);
    crawler.crawl(cat, null, pw, null);
    mess.append("\n*********************\n");
    mess.append(new String(bis.toByteArray(), CDM.utf8Charset));
  }

  private void doOneDataset( InvDataset ds) {
    if (!ds.isHarvest()) {
      messBuffer.append( " Dataset "+ ds.getName()+ " id = "+ds.getID()+" has harvest = false\n");

    } else if (isDatasetUseable( ds, messBuffer)) {
      String id = StringUtil2.replace(ds.getID(), "/", "-");
      String fileOutName = fileDir+"/"+id+".adn.xml";
      try {
        OutputStream out = new BufferedOutputStream(new FileOutputStream( fileOutName));
        writeOneItem(ds, out);
        out.close();
        messBuffer.append(" OK on Write\n");

      } catch (IOException ioe) {
        messBuffer.append("FAILED on Write "+ioe.getMessage()+"\n");
        ioe.printStackTrace();
      }
    }
  }

  public boolean isDatasetUseable(InvDataset ds, StringBuilder sbuff) {
    boolean ok = true;
    sbuff.append("Dataset "+ ds.getName()+ " id = "+ds.getID()+": ");

    if (ds.getName() == null) {
      ok = false;
      sbuff.append(" missing Name field\n");
    }

    if (ds.getUniqueID() == null) {
      ok = false;
      sbuff.append(" missing ID field\n");
    }

    List list = ds.getPublishers();
    if ((list == null) || (list.size() == 0)) {
      ok = false;
      sbuff.append(" must have publisher element that defines the data center\n");
    }

    String summary = ds.getDocumentation("summary");
    if (summary == null) {
      ok = false;
      sbuff.append(" must have documentation element of type summary\n");
    }

    String rights = ds.getDocumentation("rights");
    if (rights == null) {
      ok = false;
      sbuff.append(" must have documentation element of type rights\n");
    }

    sbuff.append(" useable= "+ok+"\n");
    return ok;
  }

  private void writeOneItem( InvDataset ds, OutputStream out) throws IOException {
    Element rootElem = new Element("itemRecord", defNS);
    Document doc = new Document(rootElem);
    writeDataset( ds, rootElem);
    rootElem.addNamespaceDeclaration(XMLEntityResolver.xsiNS);
    // rootElem.setAttribute("schemaLocation", schemaLocationLocal, XMLEntityResolver.xsiNS);
    rootElem.setAttribute("schemaLocation", defNS.getURI()+" "+schemaLocation, XMLEntityResolver.xsiNS);

    // Output the document, use standard formatter
    //XMLOutputter fmt = new XMLOutputter("  ", true);
    XMLOutputter fmt = new XMLOutputter( Format.getPrettyFormat());
    fmt.output( doc, out);
  }

  public void writeDataset(InvDataset ds, Element rootElem) {
    // general
    Element generalElem = new Element("general", defNS);
    rootElem.addContent( generalElem);

    generalElem.addContent( new Element("title", defNS).addContent("Dataset "+ds.getFullName()));
    generalElem.addContent( new Element("description", defNS).addContent(ds.getDocumentation("summary")));
    generalElem.addContent( new Element("language", defNS).addContent("en"));

    Element subjects = new Element("subjects", defNS);
    generalElem.addContent( subjects);
    subjects.addContent( new Element("subject", defNS).addContent("DLESE:Atmospheric science"));

    List list = ds.getKeywords();
    if (list.size() > 0) {
      Element keywords = new Element("keywords", defNS);
      generalElem.addContent( keywords);
      for (int i=0; i<list.size(); i++) {
        ThreddsMetadata.Vocab k = (ThreddsMetadata.Vocab) list.get(i);
        keywords.addContent( new Element("keyword", defNS).addContent( k.getText()));
      }
    }

    // life cycle
    Element lifeElem = new Element("lifecycle", defNS);
    rootElem.addContent( lifeElem);

    if ((ds.getPublishers().size() > 0) || (ds.getCreators().size() > 0)){
      Element contributors = new Element("contributors", defNS);
      lifeElem.addContent( contributors);

      list = ds.getPublishers();
      for (int i=0; i<list.size(); i++) {
        ThreddsMetadata.Source p = (ThreddsMetadata.Source) list.get(i);
        if (p.getNameVocab().getVocabulary().equalsIgnoreCase("ADN")) {
          contributors.addContent( writeSource(p, "Publisher"));
          break;
        }
      }

      list = ds.getCreators();
      for (int i=0; i<list.size(); i++) {
        ThreddsMetadata.Source p = (ThreddsMetadata.Source) list.get(i);
        if (p.getNameVocab().getVocabulary().equalsIgnoreCase("ADN")) {
          contributors.addContent( writeSource(p, "Author"));
          break;
        }
      }
    }

    // metaMetadata
    Element metaElem = new Element("metaMetadata", defNS);
    rootElem.addContent( metaElem);

    Element entries = new Element("catalogEntries", defNS);
    metaElem.addContent( entries);

    Element entry = new Element("catalog", defNS);
    entries.addContent( entry);
    String id = StringUtil2.allow(ds.getUniqueID(), ".", '-');
    entry.setAttribute("entry", id);
    entry.addContent("THREDDS");

    DateType today = new DateType(false, new Date());
    Element dateInfo = new Element("dateInfo", defNS);
    metaElem.addContent( dateInfo);
    dateInfo.setAttribute("created", today.toDateTimeStringISO());
    dateInfo.setAttribute("accessioned", today.toDateTimeStringISO());

    Element status = new Element("statusOf", defNS);
    metaElem.addContent( status);
    status.setAttribute("status", "Accessioned");

    metaElem.addContent( new Element("language", defNS).addContent("en"));
    metaElem.addContent( new Element("scheme", defNS).addContent("ADN (ADEPT/DLESE/NASA Alexandria Digital Earth Prototype/Digital Library for Earth System Education/National Aeronautics and Space Administration)"));
    metaElem.addContent( new Element("copyright", defNS).addContent("Copyright (c) 2002 UCAR (University Corporation for Atmospheric Research)"));

    Element terms = new Element("termsOfUse", defNS);
    metaElem.addContent( terms);
    terms.addContent( "Terms of use consistent with DLESE (Digital Library for Earth System Education) policy");
    terms.setAttribute( "URI", "http://www.dlese.org/documents/policy/terms_use_full.html");

    // technical
    Element technical = new Element("technical", defNS);
    rootElem.addContent( technical);

    Element online = new Element("online", defNS);
    technical.addContent( online);

    Element primaryURLelem = new Element("primaryURL", defNS);
    online.addContent( primaryURLelem);

    String href;
    if (ds instanceof InvCatalogRef) {
      InvCatalogRef catref = (InvCatalogRef) ds;
      URI uri = catref.getURI();
      href = uri.toString();
      int pos = href.lastIndexOf('.');
      href = href.substring(0,pos)+".html";

    } else {
      InvCatalogImpl cat = (InvCatalogImpl) ds.getParentCatalog();
      String catURL = cat.getBaseURI().toString();
      int pos = catURL.lastIndexOf('.');
      String catURLh = catURL.substring(0,pos)+".html";
      href = catURLh+"?dataset="+ds.getID();
    }

    primaryURLelem.addContent( href);

    Element mediums = new Element("mediums", defNS);
    online.addContent( mediums);
    mediums.addContent( new Element("medium", defNS).addContent("text/html"));

    Element reqs = new Element("requirements", defNS);
    online.addContent( reqs);
    Element req = new Element("requirement", defNS);
    reqs.addContent( req);
    req.addContent( new Element("reqType", defNS).addContent("DLESE:Other:More specific technical requirements"));

    Element oreqs = new Element("otherRequirements", defNS);
    online.addContent( oreqs);
    Element oreq = new Element("otherRequirement", defNS);
    oreqs.addContent( oreq);
    oreq.addContent( new Element("otherType", defNS).addContent("Requires data viewer tool. See the documentation page in the resource."));

    // educational
    Element educational = new Element("educational", defNS);
    rootElem.addContent( educational);

    Element audiences = new Element("audiences", defNS);
    educational.addContent( audiences);

    Element audience = new Element("audience", defNS);
    audiences.addContent( audience);
    audience.addContent( new Element("gradeRange", defNS).addContent("DLESE:Graduate or professional"));

    Element resourceTypes = new Element("resourceTypes", defNS);
    educational.addContent( resourceTypes);
    String resourceType = "DLESE:Data:In situ dataset"; // default
    if (ds.getDataType() == FeatureType.GRID)
      resourceType = "DLESE:Data:Modeled dataset"; // take a guess
    else if (ds.getDataType() == FeatureType.IMAGE)
      resourceType = "DLESE:Data:Remotely sensed dataset";
    resourceTypes.addContent( new Element("resourceType", defNS).addContent(resourceType));

    // rights
    Element rights = new Element("rights", defNS);
    rootElem.addContent( rights);

    rights.addContent( new Element("cost", defNS).addContent("DLESE:No"));
    rights.addContent( new Element("description", defNS).addContent(ds.getDocumentation("rights")));

    // geospatial
    ThreddsMetadata.GeospatialCoverage gc = ds.getGeospatialCoverage();
    if (null != gc)
      rootElem.addContent( writeGeospatialCoverage( gc));

    // temporal
    DateRange dateRange = ds.getTimeCoverage();
    if (null != dateRange)
      rootElem.addContent( writeTemporalCoverage(dateRange));
  }

  // check its an acceptable form of email
  protected boolean emailOK(ThreddsMetadata.Source p) {
    String email = p.getEmail();
    return email.indexOf('@') >= 0; // should really do a regexp
  }

  protected Element writeSource(ThreddsMetadata.Source p, String role) {
    Element contributor = new Element("contributor", defNS);
    contributor.setAttribute("role", role);

    Element organization = new Element("organization", defNS);
    contributor.addContent( organization);

    String name = p.getNameVocab().getText();
    int pos = name.indexOf('/');
    if (pos > 0) {
      organization.addContent( new Element("instName", defNS).addContent(name.substring(0,pos)));
      organization.addContent( new Element("instDept", defNS).addContent(name.substring(pos+1)));
    } else
      organization.addContent( new Element("instName", defNS).addContent(name));

    if ((p.getUrl() != null) && p.getUrl().length() > 0)
      organization.addContent( new Element("instUrl", defNS).addContent(p.getUrl()));

    if (emailOK(p))
      organization.addContent( new Element("instEmail", defNS).addContent(p.getEmail()));

    return contributor;
  }

  protected Element writeGeospatialCoverage(ThreddsMetadata.GeospatialCoverage gc) {
    Element geos = new Element("geospatialCoverages", defNS);

    Element geo = new Element("geospatialCoverage", defNS);
    geos.addContent( geo);

    Element body = new Element("body", defNS);
    geo.addContent( body);
    body.addContent( new Element("planet", defNS).addContent("Earth"));

    geo.addContent( new Element("geodeticDatumGlobalOrHorz", defNS).addContent("DLESE:WGS84"));
    geo.addContent( new Element("projection", defNS).setAttribute("type", "DLESE:Unknown"));
    geo.addContent( new Element("coordinateSystem", defNS).setAttribute("type", "DLESE:Geographic latitude and longitude"));

    Element bb = new Element("boundBox", defNS);
    geo.addContent( bb);

    double west = LatLonPointImpl.lonNormal(gc.getLonWest());
    double east = LatLonPointImpl.lonNormal(gc.getLonEast());
    bb.addContent( new Element("westCoord", defNS).setText( ucar.unidata.util.Format.dfrac( west, 2)));
    bb.addContent( new Element("eastCoord", defNS).setText( ucar.unidata.util.Format.dfrac( east, 2)));
    bb.addContent( new Element("northCoord", defNS).setText( ucar.unidata.util.Format.dfrac( gc.getLatNorth(), 2)));
    bb.addContent( new Element("southCoord", defNS).setText( ucar.unidata.util.Format.dfrac( gc.getLatSouth(), 2)));

    bb.addContent( new Element("bbSrcName", defNS).setText( "Calculated from dataset coordinate system by CDM/THREDDS"));
    return geos;
  }

  protected Element writeTemporalCoverage(DateRange dateRange) {
    Element tc = new Element("temporalCoverages", defNS);
    Element tp = new Element("timeAndPeriod", defNS);
    Element ti = new Element("timeInfo", defNS);
    tc.addContent(tp.addContent(ti));

    Element time = null;
    DateType startDate = dateRange.getStart();
    DateType endDate = dateRange.getEnd();
    if (endDate.isPresent()) {

      String units = "Days ago";
      TimeDuration duration = dateRange.getDuration();
      double value = duration.getValue("days");
      if (value > 0) {
        time = new Element("timeRelative", defNS);
        Element begin = new Element("begin", defNS);
        begin.setAttribute(CDM.UNITS, units);
        begin.addContent(Double.toString(value));
        time.addContent( begin);

        Element end = new Element("end", defNS);
        end.setAttribute(CDM.UNITS, units);
        end.addContent("0");
        time.addContent( end);
      }

    } else {
      // LOOK not tested
      time = new Element("timeAD", defNS);
      Element begin = new Element("begin", defNS);
      begin.setAttribute("date", startDate.toString());
      time.addContent( begin);

      Element end = new Element("end", defNS);
      end.setAttribute("date", endDate.toString());
      time.addContent( end);
    }

    if (time != null)
      ti.addContent(time);
    return tc;
  }

  // test
  private static void doOne( InvCatalogFactory fac, String url) {
    System.out.println("***read "+url);
    try {
        InvCatalogImpl cat = fac.readXML(url);
        StringBuilder buff = new StringBuilder();
        boolean isValid = cat.check( buff, false);
        System.out.println("catalog <" + cat.getName()+ "> "+ (isValid ? "is" : "is not") + " valid");
        System.out.println(" validation output=\n" + buff);
        // System.out.println(" catalog=\n" + fac.writeXML(cat));

        ADNWriter w = new ADNWriter();
        StringBuilder sbuff = new StringBuilder();
        w.writeDatasetEntries( cat, "C:/temp/adn3", sbuff);
        System.out.println(" messages=\n"+sbuff);

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

  }

   /** testing */
  public static void main (String[] args) throws Exception {
    InvCatalogFactory catFactory = InvCatalogFactory.getDefaultFactory(true);
    doOne(catFactory, "http://thredds.ucar.edu/thredds/idd/models.xml");
  }

}


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

package ucar.unidata.geoloc.ogc;

import ucar.nc2.units.SimpleUnit;

import ucar.nc2.util.IO;
import ucar.unidata.geoloc.ProjectionImpl;
import ucar.unidata.geoloc.projection.*;

import java.io.IOException;
import java.text.ParseException;

/**
 * This class parses OGC WKT Spatial Reference Text.
 *
 * @author Barrodale Computing Services, Ltd. (Eric Davies)
 * @author Unidata Java Development Team
 */
public class WKTParser {

  /*
  * geogcs info
  */

  /**
   * geo coord sys name
   */
  private String geogcsName;

  /**
   * datum name
   */
  private String datumName;

  /**
   * spheriod name
   */
  private String spheroidName;

  /**
   * major axis, inverse minor axis
   */
  private double majorAxis, inverseMinor;

  /**
   * primeMeridian name
   */
  private String primeMeridianName;

  /**
   * primeMeridian name
   */
  private double primeMeridianValue;

  /**
   * geographic unit name
   */
  private String geogUnitName;

  /**
   * geographic unit value
   */
  private double geogUnitValue;

  /**
   * is this a projection
   */
  private boolean isAProjection;

  /**
   * projection name
   */
  private String projName;

  /**
   * projection type
   */
  private String projectionType;

  /**
   * projection parameters
   */
  private java.util.HashMap parameters = new java.util.HashMap();

  /**
   * projection unit name
   */
  private String projUnitName;

  /**
   * projection unit value
   */
  private double projUnitValue;

  /**
   * parse position
   */
  private int position = 0;

  /**
   * the reader
   */
  java.io.StringReader reader;

  /**
   * Creates a new instance of WKTParser. If the constructor
   * succeeds, the spatial reference text was successfully parsed.
   *
   * @param srtext The spatial reference text to be parsed.
   *               Geocentric coordinate text is not currently supported.
   * @throws ParseException A ParseException is thrown
   *                        if the spatial reference text could not be parsed.
   */
  public WKTParser(String srtext) throws ParseException {
    reader = new java.io.StringReader(srtext);

    if (srtext.startsWith("PROJCS")) {
      isAProjection = true;
      parseProjcs();
    } else {
      isAProjection = false;
      parseGeogcs();
    }
  }

  /**
   * Peek ahead
   *
   * @return the char
   * @throws ParseException problem parsing
   */
  private char peek() throws ParseException {
    try {
      reader.mark(10);
      int aChar = reader.read();
      reader.reset();
      if (aChar < 0) {
        return (char) 0;
      } else {
        return (char) aChar;
      }
    } catch (java.io.IOException e1) {
      throw new ParseException("Strange io error " + e1, position);
    }
  }

  /**
   * _more_
   *
   * @return _more_
   * @throws ParseException _more_
   */
  private char getChar() throws ParseException {
    try {
      int val = reader.read();
      position++;
      if (val < 0) {
        throw new ParseException("unexpected eof of srtext",
                position);
      }
      return (char) val;
    } catch (java.io.IOException e1) {
      throw new ParseException(e1.toString(), position);
    }

  }

  /**
   * _more_
   *
   * @param literal _more_
   * @throws ParseException _more_
   */
  private void eatLiteral(String literal) throws ParseException {
    int n = literal.length();
    for (int i = 0; i < n; i++) {
      char v = getChar();
      if (v != literal.charAt(i)) {
        throw new ParseException("bad srtext", position);
      }
    }
  }

  /**
   * _more_
   *
   * @return _more_
   * @throws ParseException _more_
   */
  private double eatReal() throws ParseException {
    StringBuilder b = new StringBuilder();
    for (; ; ) {
      char t = peek();
      if (Character.isDigit(t) || (t == 'e') || (t == 'E')
              || (t == '.') || (t == '-') || (t == '+')) {
        b.append(getChar());
      } else {
        break;
      }
    }
    try {
      return Double.parseDouble(b.toString());
    } catch (NumberFormatException e1) {
      throw new ParseException("bad number" + e1, position);
    }
  }

  /**
   * _more_
   *
   * @return _more_
   * @throws ParseException _more_
   */
  private String eatString() throws ParseException {
    StringBuilder b = new StringBuilder();
    if (getChar() != '"') {
      throw new ParseException("expected string", position);
    }
    for (; ; ) {
      char t = getChar();
      if (t == '"') {
        break;
      }
      b.append(t);
    }
    return b.toString();
  }

  /**
   * _more_
   *
   * @return _more_
   * @throws ParseException _more_
   */
  private String eatTerm() throws ParseException {
    StringBuilder b = new StringBuilder();
    for (; ; ) {
      char val = peek();
      if (!Character.isJavaIdentifierPart(val)) {
        break;
      }
      b.append(getChar());
    }
    return b.toString();
  }

  /**
   * _more_
   *
   * @throws ParseException _more_
   */
  private void eatComma() throws ParseException {
    if (getChar() != ',') {
      throw new ParseException("expected comma", position);
    }
  }

  /**
   * _more_
   *
   * @throws ParseException _more_
   */
  private void eatOpenBrace() throws ParseException {
    if (getChar() != '[') {
      throw new ParseException("expected [", position);
    }
  }

  /**
   * _more_
   *
   * @throws ParseException _more_
   */
  private void eatCloseBrace() throws ParseException {
    if (getChar() != ']') {
      throw new ParseException("expected ]", position);
    }
  }

  /**
   * _more_
   *
   * @throws ParseException _more_
   */
  private void parseProjcs() throws ParseException {
    eatLiteral("PROJCS[");
    projName = eatString();
    eatComma();
    parseGeogcs();
    for (; ; ) {
      char next = getChar();
      if (next == ']') {
        break;
      } else if (next != ',') {
        throw new ParseException("expected , or ]", position);
      } else {
        String term = eatTerm();
        if ("PARAMETER".equals(term)) {
          eatParameter();
        } else if ("UNIT".equals(term)) {
          eatProjcsUnit();
        } else if ("PROJECTION".equals(term)) {
          eatProjectionType();
        }
      }
    }
  }

  /**
   * _more_
   *
   * @throws ParseException _more_
   */
  private void eatParameter() throws ParseException {
    eatOpenBrace();
    String parameterName = eatString();
    eatComma();
    Double value = eatReal();
    eatCloseBrace();
    parameters.put(parameterName.toLowerCase(), value);
  }

  /**
   * _more_
   *
   * @throws ParseException _more_
   */
  private void eatProjcsUnit() throws ParseException {
    eatOpenBrace();
    projUnitName = eatString();
    eatComma();
    projUnitValue = eatReal();
    eatCloseBrace();
  }

  /**
   * _more_
   *
   * @throws ParseException _more_
   */
  private void eatProjectionType() throws ParseException {
    eatOpenBrace();
    projectionType = eatString();
    eatCloseBrace();
  }

  /**
   * _more_
   *
   * @throws ParseException _more_
   */
  private void parseGeogcs() throws ParseException {
    eatLiteral("GEOGCS[");
    geogcsName = eatString();
    for (; ; ) {
      char t = getChar();
      if (t == ']') {
        break;
      } else if (t != ',') {
        throw new ParseException("expected , or ]", position);
      } else {
        String term = eatTerm();
        if ("DATUM".equals(term)) {
          eatDatum();
        } else if ("PRIMEM".equals(term)) {
          eatPrimem();
        } else if ("UNIT".equals(term)) {
          eatUnit();
        }
      }
    }
  }

  /**
   * _more_
   *
   * @throws ParseException _more_
   */
  private void eatDatum() throws ParseException {
    eatOpenBrace();
    datumName = eatString();
    eatComma();
    eatSpheroid();
    eatCloseBrace();
  }

  /**
   * _more_
   *
   * @throws ParseException _more_
   */
  private void eatPrimem() throws ParseException {
    eatOpenBrace();
    primeMeridianName = eatString();
    eatComma();
    primeMeridianValue = eatReal();
    eatCloseBrace();
  }

  /**
   * _more_
   *
   * @throws ParseException _more_
   */
  private void eatSpheroid() throws ParseException {
    eatLiteral("SPHEROID");
    eatOpenBrace();
    spheroidName = eatString();
    eatComma();
    majorAxis = eatReal();
    eatComma();
    inverseMinor = eatReal();
    eatCloseBrace();
  }

  /**
   * _more_
   *
   * @throws ParseException _more_
   */
  private void eatUnit() throws ParseException {
    eatOpenBrace();
    geogUnitName = eatString();
    eatComma();
    geogUnitValue = eatReal();
    eatCloseBrace();
  }

  /**
   * Get the name of the geographic coordinate system.
   *
   * @return the name.
   */
  public String getGeogcsName() {
    return geogcsName;
  }

  /**
   * Get the datum name. Note that the datum name itself implies information
   * not found in the spheroid.
   *
   * @return The name of the datum.
   */
  public String getDatumName() {
    return datumName;
  }

  /**
   * Get the name of the spheroid.
   *
   * @return The name of the spheroid.
   */
  public String getSpheroidName() {
    return spheroidName;
  }

  /**
   * Get the major axis of the spheroid.
   *
   * @return The major axis of the spheroid, in meters.
   */
  public double getMajorAxis() {
    return majorAxis;
  }

  /**
   * Get the inverse flattening.
   *
   * @return The inverse flattening. Note that this is unitless.
   */
  public double getInverseFlattening() {
    return inverseMinor;
  }

  /**
   * Get the name of the prime meridian.
   *
   * @return the name of the prime meridian. Usually "Greenwich".
   */
  public String getPrimeMeridianName() {
    return primeMeridianName;
  }

  /**
   * Return the value of prime meridian.
   *
   * @return The longitude of the prime meridian, usually 0.
   */
  public double getPrimeMeridianValue() {
    return primeMeridianValue;
  }

  /**
   * Get the name of the unit that the prime meridian is expressed in.
   *
   * @return Tje name of the unit that the prime meridian is expressed in. Usually "Degree".
   */
  public String getGeogUnitName() {
    return geogUnitName;
  }

  /**
   * Get the size of the unit that the prime meridian is expressed in.
   *
   * @return The conversion from the prime meridian units to radians.
   */
  public double getGeogUnitValue() {
    return geogUnitValue;
  }

  /**
   * Inquire if a particular projection parameter is present.
   *
   * @param name The name of the parameter. Case is ignored.
   * @return True if the parameter is present.
   */
  public boolean hasParameter(String name) {
    return (parameters.get(name.toLowerCase()) != null);
  }

  /**
   * Get the value of the projection parameter. An IllegalArgument exception
   * is thrown if the parameter is not found.
   *
   * @param name The name of the parameter. Case is ignored.
   * @return The value of the parameter, as a double.
   */
  public double getParameter(String name) {
    Double val = (Double) parameters.get(name.toLowerCase());
    if (val == null) {
      throw new IllegalArgumentException("no parameter called " + name);
    }
    return val.doubleValue();
  }

  /**
   * Determine if the spatial reference text defines a planar projection,
   * as opposed to a Geographic coordinate system.
   *
   * @return True if the spatial reference text defines a planar projection system.
   */
  public boolean isPlanarProjection() {
    return isAProjection;
  }

  /**
   * Get the name of the projection.
   *
   * @return The name of the projection.
   */
  public String getProjName() {
    return projName;
  }

  /**
   * Get the name of the type of projection.
   *
   * @return Returns the name of the type of the projection. For example,Transverse_Mercator.
   *         Returns null for geographic coordinate systems.
   */
  public String getProjectionType() {
    return projectionType;
  }

  /**
   * Get the name of the projection unit.
   *
   * @return Get the name of the projection unit. Usually "Meter".
   */
  public String getProjUnitName() {
    return projUnitName;
  }

  /**
   * Get the projection unit value.
   *
   * @return The size of the projection unit, in meters.
   */
  public double getProjUnitValue() {
    return projUnitValue;
  }

  /**
   * Convert OGC spatial reference WKT to a ProjectionImpl.
   * An IllegalArgumentException may be thrown if a parameter is missing.
   *
   * @param srp The parsed OGC WKT spatial reference text.
   * @return The ProjectionImpl class.
   * @throws java.text.ParseException If the OGIS spatial reference text was not parseable.
   */
  public static ProjectionImpl convertWKTToProjection(WKTParser srp) {
    if (!srp.isPlanarProjection()) {
      return new ucar.unidata.geoloc.projection.LatLonProjection();
    } else {
      String projectionType = srp.getProjectionType();
      double falseEasting = 0;
      double falseNorthing = 0;
      ProjectionImpl proj = null;
      if (srp.hasParameter("False_Easting")) {
        falseEasting = srp.getParameter("False_Easting");
      }
      if (srp.hasParameter("False_Northing")) {
        falseNorthing = srp.getParameter("False_Northing");
      }
      if ((falseEasting != 0.0) || (falseNorthing != 0.0)) {
        double scalef = 1.0;
        if (srp.getProjUnitName() != null) {
          try {
            SimpleUnit unit =
                    SimpleUnit.factoryWithExceptions(
                            srp.getProjUnitName());
            scalef = unit.convertTo(srp.getProjUnitValue(),
                    SimpleUnit.kmUnit);
          } catch (Exception e) {
            System.out.println(srp.getProjUnitValue() + " "
                    + srp.getProjUnitName()
                    + " not convertible to km");
          }
        }
        falseEasting *= scalef;
        falseNorthing *= scalef;
      }

      if (srp.getProjName().contains("UTM_Zone_"))
        return processUTM(srp);

      if ("Transverse_Mercator".equals(projectionType)) {
        double lat0 = srp.getParameter("Latitude_Of_Origin");
        double scale = srp.getParameter("Scale_Factor");
        double tangentLon = srp.getParameter("Central_Meridian");
        proj = new TransverseMercator(lat0, tangentLon, scale, falseEasting, falseNorthing);

      } else if ("Lambert_Conformal_Conic".equals(projectionType)) {
        double lon0 = srp.getParameter("Central_Meridian");
        double par1 = srp.getParameter("Standard_Parallel_1");
        double par2 = par1;
        if (srp.hasParameter("Standard_Parallel_2")) {
          par2 = srp.getParameter("Standard_Parallel_2");
        }
        double lat0 = srp.getParameter("Latitude_Of_Origin");
        return new LambertConformal(lat0, lon0, par1, par2, falseEasting, falseNorthing);

      } else if ("Albers".equals(projectionType)) {
        double lon0 = srp.getParameter("Central_Meridian");
        double par1 = srp.getParameter("Standard_Parallel_1");
        double par2 = par1;
        if (srp.hasParameter("Standard_Parallel_2")) {
          par2 = srp.getParameter("Standard_Parallel_2");
        }
        double lat0 = srp.getParameter("Latitude_Of_Origin");
        return new AlbersEqualArea(lat0, lon0, par1, par2, falseEasting, falseNorthing);

      } else if ("Stereographic".equals(projectionType)) {
        double lont = srp.getParameter("Central_Meridian");
        double scale = srp.getParameter("Scale_Factor");
        double latt = srp.getParameter("Latitude_Of_Origin");
        return new Stereographic(latt, lont, scale, falseEasting, falseNorthing);

      } else if ("Mercator".equals(projectionType)) {
        double lat0 = srp.getParameter("Latitude_Of_Origin");
        double lon0 = srp.getParameter("Central_Meridian");
        proj = new Mercator(lon0, lat0, falseEasting, falseNorthing);

      } else if ("Universal_Transverse_Mercator".equals(projectionType)) {
        //throw new java.text.ParseException(
        //    "UTM adapter not implemented yet", 0);
      }
      return proj;
    }

  }

  static ProjectionImpl processUTM(WKTParser srp) {
  // NAD_1983_UTM_Zone_12N
      String name = srp.getProjName();
      int pos = name.indexOf("UTM_Zone_");
      String zoneS = name.substring(pos + 9);
      char lastC;
      int zone = Integer.parseInt(zoneS.substring(0, zoneS.length()-1));
      lastC = zoneS.charAt(zoneS.length()-1);
      boolean isNorth = (lastC =='N');

      return new UtmProjection(zone, isNorth);
  }

  public static void main(String[] args) throws IOException, ParseException {
    String testFile = "E:/work/yuan/shapefile/SkiAreaBoundaries.prj";
    //String filename = ( args != args[0] == null)  ? testFile : args[0];

    String contents = IO.readFile(testFile);
    System.out.printf("%s%n", contents);

    WKTParser p = new WKTParser(contents);
    ProjectionImpl proj = convertWKTToProjection(p);
    System.out.printf("%s%n", proj);
  }
}


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

package ucar.nc2.grib.grib1;

import javax.annotation.Nullable;
import ucar.nc2.grib.GribNumbers;
import ucar.unidata.io.RandomAccessFile;

import java.io.IOException;
import java.util.Formatter;
import java.util.zip.CRC32;

/**
 * The Grid Definition Section for GRIB-1 files
 * Effectively immutable, but caching lazy gds
 * @author caron
 */

// @Immutable gds makes it not immutable
public class Grib1SectionGridDefinition {
  private final byte[] rawData;
  private final long startingPosition;
  private final int gridTemplate; // octet 6

  private final int predefinedGridDefinition; // = -1 if not used
  private final int predefinedGridDefinitionCenter; // = -1 if not used

  /**
   * Read Grib Definition section from raf.
   *
   * @param raf RandomAccessFile, with pointer at start of section
   * @throws java.io.IOException      on I/O error
   * @throws IllegalArgumentException if not a GRIB-2 record
   */
  public Grib1SectionGridDefinition(RandomAccessFile raf) throws IOException {

    startingPosition = raf.getFilePointer();

    // octets 1-3 (Length of GDS)
    int length = GribNumbers.int3(raf);

    // octet 6
    raf.skipBytes(2);
    gridTemplate = GribNumbers.uint(raf);

    // read in whole GDS as byte[]
    rawData = new byte[length];
    raf.seek(startingPosition);
    raf.readFully(rawData);

    predefinedGridDefinition = -1;
    predefinedGridDefinitionCenter = -1;
  }

  /**
   * Set Grib Definition section from byte array.
   *
   * @param rawData the byte array
   */
  public Grib1SectionGridDefinition(byte[] rawData) {
    this.rawData = rawData;
    this.gridTemplate = getOctet(6);
    this.startingPosition = -1;
    predefinedGridDefinition = -1;
    predefinedGridDefinitionCenter = -1;
  }

  public Grib1SectionGridDefinition(Grib1SectionProductDefinition pds) {
    startingPosition = -1;
    gridTemplate = -pds.getGridDefinition(); // LOOK ??
    rawData = null;
    predefinedGridDefinitionCenter = pds.getCenter();
    predefinedGridDefinition = pds.getGridDefinition();
  }

  /**
   * get the raw bytes of the GDS
   *
   * @return GDS as byte[]
   */
  public byte[] getRawBytes() {
    return rawData;
  }

  /**
   * Calculate the CRC of the entire byte array
   *
   * @return CRC  of the entire byte array
   */
  public long calcCRC() {
    long crc;
    if (rawData == null)
      crc = predefinedGridDefinitionCenter << 16 + predefinedGridDefinition;
    else {
      CRC32 crc32 = new CRC32();
      crc32.update(rawData);
      crc = crc32.getValue();
    }
    return crc;
  }

  public int getLength() {
    return (rawData == null) ? 0 : rawData.length;
  }

  public long getOffset() {
    return startingPosition;
  }

  /**
   * Get Grid Template number (code table 6)
   *
   * @return Grid Template number.
   */
  public int getGridTemplate() {
    return gridTemplate;
  }

  /**
   * From pds.getGridDefinition
   * @return  pds.getGridDefinition, or -1 if uses a real Grib1SectionGridDefinition
   */
  public int getPredefinedGridDefinition() {
    return predefinedGridDefinition;
  }

  private int getOctet(int index) {
    if (rawData == null)
      return 255;   // predefined
    if (index > rawData.length) return GribNumbers.UNDEFINED;
    return rawData[index - 1] & 0xff;
  }

  private Grib1Gds gds = null;
  public synchronized Grib1Gds getGDS() {
    if (gds != null) return gds;

    if (predefinedGridDefinition != -1) {
      gds = ucar.nc2.grib.grib1.Grib1GdsPredefined.factory(predefinedGridDefinitionCenter, predefinedGridDefinition);
      return gds;
    }

    gds = Grib1Gds.factory(gridTemplate, rawData);
    if (isThin()) {
      gds.setNptsInLine(getNptsInLine(gds));
    }
    return gds;
  }

  //////////////////////////////////
  // the infinite grib1 baloney

  /**
   * is a "thin" grid
   *
   * @return if a thin grid
   */
  public final boolean isThin() {
    if (rawData == null) return false;
    int octet5 = getOctet(5);
    int nv = getOctet(4);
    return (octet5 != 255) && (nv == 0 || nv == 255);
  }

  /**
   * Gets the number of points in each line for Quasi/Thin grids
   * "List of numbers of points in each row (length = NROWS x 2 octets, where NROWS is the
   *  total number of rows defined within the grid description)"
   * @return number of points in each line as int[]
   */
  private int[] getNptsInLine(Grib1Gds gds) {
    int numPts;
    if ((gds.getScanMode() & 32) == 0) { // bit3 = 0 : Adjacent points in i direction are consecutive
      numPts = gds.getNy();
    } else {                             // bit3 = 1 : Adjacent points in j direction are consecutive
      numPts = gds.getNx();
    }

    int[] parallels = new int[numPts];
    int offset = getOctet(5);
    for (int i = 0; i < numPts; i++) {
      parallels[i] = GribNumbers.int2(getOctet(offset++), getOctet(offset++));
    }
    return parallels;
  }

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

  public boolean hasVerticalCoordinateParameters() {
    if (rawData == null) return false;
    int octet5 = getOctet(5);
    int nv = getOctet(4);
    return (octet5 != 255) && (nv != 0 && nv != 255);
  }

  @Nullable
  private double[] getVerticalCoordinateParameters() {
    if (!hasVerticalCoordinateParameters()) return null;

    int offset = getOctet(5);
    int n = getOctet(4);
    double[] vertCoords = new double[ n ];
    for (int i = 0; i < n; i++) {
      vertCoords[i] = GribNumbers.float4(getOctet(offset++), getOctet(offset++), getOctet(offset++), getOctet(offset++)) ;
    }
    return vertCoords;
  }

  @Override
  public String toString() {
    try (Formatter sb = new Formatter()) {
      sb.format("Grib1SectionGridDefinition");
      sb.format("  gridTemplate=%d%n", gridTemplate);
      if (predefinedGridDefinition >= 0) {
        sb.format("  predefinedGridDefinition=%d%n", predefinedGridDefinition);
      }
      double[] verts = getVerticalCoordinateParameters();
      if (verts != null) {
        sb.format("  verticalPressureLevels (%d)=", verts.length);
        for (double d : verts) {
          sb.format("%10.4f ", d);
        }
        sb.format("%n");
      }
      sb.format("%n");
      return sb.toString();
    }
  }
}


/* Copyright 2012, UCAR/Unidata.
   See the LICENSE file for more information.
*/

package dap4.dap4lib;

import dap4.core.data.DSP;
import dap4.core.data.DataCursor;
import dap4.core.dmr.DapNode;
import dap4.core.dmr.DapStructure;
import dap4.core.dmr.DapType;
import dap4.core.dmr.DapVariable;
import dap4.core.util.DapException;
import dap4.core.util.DapSort;
import dap4.core.util.Index;
import dap4.core.util.Slice;

import java.util.List;

abstract public class AbstractCursor implements DataCursor
{
    //////////////////////////////////////////////////
    // Constants

    //////////////////////////////////////////////////
    // Instance Variables

    protected Scheme scheme;

    protected DSP dsp;
    protected DapNode template;

    protected Index arrayindex = null;
    protected long recordindex = -1; // scheme == record

    protected AbstractCursor container = null;

    protected long recordcount = -1;

    //////////////////////////////////////////////////
    // Constructor(s)

    public AbstractCursor(Scheme scheme, DSP dsp, DapNode template, AbstractCursor container)
    {
        setScheme(scheme);
        setDSP(dsp);
        setTemplate(template);
        setContainer(container);
    }

    public AbstractCursor(AbstractCursor orig)
    {
        this(orig.getScheme(), orig.getDSP(), orig.getTemplate(), (AbstractCursor) orig.getContainer());
    }

    public String toString()
    {
        StringBuilder buf = new StringBuilder();
        buf.append(getScheme().toString());
        if(getScheme() == Scheme.STRUCTARRAY || getScheme() == Scheme.SEQARRAY)
            buf.append("[]");
        buf.append(":");
        buf.append(getTemplate().toString());
        if(this.arrayindex != null) {
            buf.append("::");
            buf.append(this.arrayindex.toString());
        }
        if(this.recordindex >= 0) {
            buf.append("*");
            buf.append(this.recordindex);
        }
        return buf.toString();
    }

    //////////////////////////////////////////////////
    // Unimplemented DataCursor API methods

    @Override
    abstract public Object read(List<Slice> slices) throws DapException;

    @Override
    abstract public Object read(Index index) throws DapException;

    @Override
    abstract public AbstractCursor readRecord(long i) throws DapException;

    @Override
    abstract public AbstractCursor readField(int fieldindex) throws DapException;

    //////////////////////////////////////////////////
    // Selected DataCursor API overrides

    @Override
    public int fieldIndex(String name)
            throws DapException
    {
        DapStructure ds;
        if(getTemplate().getSort().isCompound())
            ds = (DapStructure) getTemplate();
        else if(getTemplate().getSort().isVar()
                && (((DapVariable) getTemplate()).getBaseType().getSort().isCompound()))
            ds = (DapStructure) ((DapVariable) getTemplate()).getBaseType();
        else
            throw new DapException("Attempt to get field name on non-compound object");
        int i = ds.indexByName(name);
        if(i < 0)
            throw new DapException("Unknown field name: " + name);
        return i;
    }

    @Override
    public Scheme getScheme()
    {
        return this.scheme;
    }

    @Override
    public DSP
    getDSP()
    {
        return this.dsp;
    }

    @Override
    public DapNode
    getTemplate()
    {
        return this.template;
    }

    @Override
    public Index getIndex()
            throws DapException
    {
        if(this.scheme != Scheme.STRUCTURE && this.scheme != Scheme.SEQUENCE)
            throw new DapException("Not a Sequence|Structure instance");
        return this.arrayindex;
    }

    @Override
    public long getRecordIndex()
            throws DapException
    {
        if(this.scheme != Scheme.RECORD)
            throw new DapException("Not a Record instance");
        return this.recordindex;
    }

    @Override
    public long
    getRecordCount()
            throws DapException
    {
        if(this.scheme != Scheme.SEQUENCE)
            throw new DapException("Not a SEQUENCE instance");
        return this.recordcount;
    }

    @Override
    public AbstractCursor
    getContainer()
    {
        return this.container;
    }

    @Override
    public boolean
    isScalar()
    {
        if(getTemplate().getSort().isVar()) {
            return ((DapVariable) getTemplate()).getRank() == 0;
        } else
            return false;
    }

    public boolean isField()
    {
        return getTemplate().getContainer() != null;
    }

    public boolean isAtomic()
    {
        boolean is = this.scheme == Scheme.ATOMIC;
        assert !is || getTemplate().getSort() == DapSort.ATOMICTYPE
                || (getTemplate().getSort() == DapSort.VARIABLE
                && ((DapVariable) getTemplate()).getBaseType().getTypeSort().isAtomic());
        return is;
    }

    public boolean isCompound()
    {
        boolean is = (this.scheme == Scheme.SEQUENCE || this.scheme == Scheme.STRUCTURE);
        assert !is
                || getTemplate().getSort() == DapSort.SEQUENCE
                || getTemplate().getSort() == DapSort.STRUCTURE
                || (getTemplate().getSort() == DapSort.VARIABLE
                && ((DapVariable) getTemplate()).getBaseType().getTypeSort().isCompoundType());
        return is;
    }

    public boolean isCompoundArray()
    {
        boolean is = (this.scheme == Scheme.SEQARRAY || this.scheme == Scheme.STRUCTARRAY);
        assert !is
                || getTemplate().getSort() == DapSort.SEQUENCE
                || getTemplate().getSort() == DapSort.STRUCTURE
                || (getTemplate().getSort() == DapSort.VARIABLE
                && ((DapVariable) getTemplate()).getBaseType().getTypeSort().isCompoundType());
        return is;
    }

    //////////////////////////////////////////////////
    // AbstractCursor extensions

    public AbstractCursor
    setIndex(Index index)
    {
        this.arrayindex = index;
        return this;
    }

    public AbstractCursor
    setRecordIndex(long index)
    {
        this.recordindex = index;
        return this;
    }

    public AbstractCursor
    setRecordCount(long count)
    {
        this.recordcount = count;
        return this;
    }

    public AbstractCursor
    setContainer(AbstractCursor container)
    {
        this.container = container;
        return this;
    }

    public AbstractCursor
    setScheme(Scheme scheme)
    {
        this.scheme = scheme;
        return this;
    }

    public AbstractCursor
    setDSP(DSP dsp)
    {
        this.dsp = dsp;
        return this;
    }

    public AbstractCursor
    setTemplate(DapNode template)
    {
        this.template = template;
        return this;
    }

    //////////////////////////////////////////////////
    // Utilities

    static public Scheme
    schemeFor(DapVariable field)
    {
        DapType ftype = field.getBaseType();
        Scheme scheme = null;
        boolean isscalar = field.getRank() == 0;
        if(ftype.getTypeSort().isAtomic())
            scheme = Scheme.ATOMIC;
        else {
            if(ftype.getTypeSort().isStructType()) scheme = Scheme.STRUCTARRAY;
            else if(ftype.getTypeSort().isSeqType()) scheme = Scheme.SEQARRAY;
        }
        return scheme;
    }
}