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

package thredds.featurecollection;

import thredds.client.catalog.*;
import thredds.client.catalog.builder.CatalogBuilder;
import thredds.client.catalog.builder.CatalogRefBuilder;
import thredds.client.catalog.builder.DatasetBuilder;
import thredds.inventory.CollectionSpecParser;
import thredds.inventory.CollectionUpdateType;
import thredds.inventory.MFile;
import thredds.server.catalog.FeatureCollectionRef;
import ucar.nc2.grib.coord.CoordinateRuntime;
import ucar.nc2.constants.DataFormatType;
import ucar.nc2.constants.FeatureType;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.dt.grid.GridCoordSys;
import ucar.nc2.ft2.coverage.CoverageCollection;
import ucar.nc2.grib.GdsHorizCoordSys;
import ucar.nc2.grib.collection.GribCdmIndex;
import ucar.nc2.grib.collection.GribCollectionImmutable;
import ucar.nc2.grib.collection.PartitionCollectionImmutable;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.time.CalendarDateRange;
import ucar.nc2.units.DateRange;
import ucar.nc2.units.DateType;
import ucar.unidata.geoloc.LatLonRect;

import javax.annotation.concurrent.ThreadSafe;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
import java.util.*;

/**
 * Implement FeatureCollection GRIB - a collection of Grib1 or Grib2 files that are served as Grids.
 *

 * catalogs                                see makeCatalog()
 * path/catalog.xml                       // top catalog
 * path/partitionName/catalog.xml
 * path/partitionName/../partitionName/catalog.xml
 * path/latest.xml                       // latest (resolver)
 *

 * datasets                                see findDataset()
 * path/dataset (dataset = BEST, TWOD, TP, "")  // top collection, single group
 * path/dataset/groupName                       // top collection, multiple group
 * path/partitionName/dataset                   // partition, single group
 * path/partitionName/../partitionName/dataset
 * path/partitionName/dataset/groupName         // partition, multiple group
 * path/partitionName/../partitionName/dataset/groupName
 *

 * files
 * path/partitionName/../partitionName/FILES/filename
 *
 * @author caron
 * @since 4/15/11
 */
@ThreadSafe
public class InvDatasetFcGrib extends InvDatasetFeatureCollection {
  private static org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(InvDatasetFcGrib.class);

  static private final String COLLECTION = "collection";

  static private final String BEST_DATASET = GribCollectionImmutable.Type.Best.toString();
  static private final String TWOD_DATASET = GribCollectionImmutable.Type.TwoD.toString();
  static private final String PARTITION_DATASET = "TP";
  static private final String COLLECTION_DATASET = "GC";

  /////////////////////////////////////////////////////////////////////////////
  protected class StateGrib extends State {
    GribCollectionImmutable gribCollection;   // top level
    GribCollectionImmutable latest;
    String latestPath;

    protected StateGrib(StateGrib from) {
      super(from);
      if (from != null) {
        this.gribCollection = from.gribCollection;
        this.latest = from.latest;
        this.latestPath = from.latestPath;
      }
    }

    @Override
    public State copy() {
      return new StateGrib(this);
    }
  }

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

  public InvDatasetFcGrib(FeatureCollectionRef parent, FeatureCollectionConfig config) {
    super(parent, config);

    Formatter errlog = new Formatter();
    CollectionSpecParser sp = config.getCollectionSpecParser(errlog);
    topDirectory = sp.getRootDir();

    String errs = errlog.toString();
    if (errs.length() > 0) logger.warn("{}: CollectionManager parse error = {} ", name, errs);

    state = new StateGrib(null);
  }

  @Override
  public void close() {
    if (state != null) {
      StateGrib stateGrib = (StateGrib) state;
      if (stateGrib.gribCollection != null)
        try {
          stateGrib.gribCollection.close();
        } catch (IOException e) {
          logger.error("Cant close {}", stateGrib.gribCollection.getName(), e);
        }
    }
    super.close();
  }

  @Override
  protected void _showStatus(Formatter f, boolean summaryOnly, String type) throws IOException {
    StateGrib localState;
    synchronized (lock) {
      localState = (StateGrib) state;
    }
    if (localState.gribCollection != null) {
      if (summaryOnly)
        localState.gribCollection.showStatusSummary(f, type);
      else
        localState.gribCollection.showStatus(f);
    }
  }

  @Override
  protected void updateCollection(State state, CollectionUpdateType force) {
    try {
      StateGrib localState = (StateGrib) state;
      GribCollectionImmutable previous = localState.gribCollection;
      GribCollectionImmutable previousLatest = localState.latest;

      localState.latest = null; // will get updated next time its asked for
      localState.gribCollection = GribCdmIndex.openGribCollection(this.config, force, logger);
      if (localState.gribCollection == null)
        logger.error("InvDatasetFcGrib.updateCollection failed " + this.config);

      logger.debug("{}: GribCollection object was recreated", name);
      if (previous != null)
        previous.close();                 // LOOK may be another thread using - other thread will fail
      if (previousLatest != null) previousLatest.close();

    } catch (IOException ioe) {
      logger.error("GribFc updateCollection", ioe);
    }
  }

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

  private String makeCollectionShortName(String collectionName) {
    String topCollectionName = config.collectionName;
    if (collectionName.equals(topCollectionName)) return topCollectionName;
    if (collectionName.startsWith(topCollectionName)) {
      return name + collectionName.substring(topCollectionName.length());
    }
    return collectionName;
  }

  private DatasetBuilder makeDatasetFromCollection(URI catURI, boolean isTop, CatalogBuilder parentCatalog, String parentCollectionName, GribCollectionImmutable fromGc) throws IOException {
    if (fromGc == null)
      throw new FileNotFoundException("Grib Collection '" + getName() + "' does not exist or is empty");

    String dsName = isTop ? name : makeCollectionShortName(fromGc.getName());
    DatasetBuilder result = new DatasetBuilder(null);
    result.setName(dsName);
    if (parent != null)
      result.transferInheritedMetadata(parent); // make all inherited metadata local
    result.addServiceToCatalog(virtualService);

    String tpath = config.path + "/" + COLLECTION;
    ThreddsMetadata tmi = result.getInheritableMetadata();  // LOOK should we be allowed to modify this ??
    tmi.set(Dataset.VariableMapLinkURI, makeUriResolved(catURI, makeMetadataLink(tpath, VARIABLES)));
    tmi.set(Dataset.ServiceName, virtualService.getName());
    tmi.set(Dataset.DataFormatType, fromGc.isGrib1 ? DataFormatType.GRIB1.getDescription() : DataFormatType.GRIB2.getDescription());
    tmi.set(Dataset.Properties, Property.convertToProperties(fromGc.getGlobalAttributes()));
    tmi.set(Dataset.FeatureType, FeatureType.GRID.toString()); // override GRIB

    String pathStart = parentCollectionName == null ? config.path : config.path + "/" + parentCollectionName;

    for (GribCollectionImmutable.Dataset ds : fromGc.getDatasets()) {
      boolean isSingleGroup = ds.getGroupsSize() == 1;
      Iterable<GribCollectionImmutable.GroupGC> groups = ds.getGroups();

      if (ds.getType() == GribCollectionImmutable.Type.TwoD) {

        if (config.gribConfig.hasDatasetType(FeatureCollectionConfig.GribDatasetType.TwoD)) {
          DatasetBuilder twoD = new DatasetBuilder(result);
          twoD.setName(getDatasetNameTwoD(result.getName()));
          String path = pathStart + "/" + TWOD_DATASET;
          // twoD.put(Dataset.UrlPath, path);
          twoD.addToList(Dataset.Documentation, new Documentation(null, null, null, "summary", "Two time dimensions: reference and forecast; full access to all GRIB records"));
          result.addDataset(twoD);

          // Collections.sort(groups);
          makeDatasetsFromGroups(catURI, twoD, path, groups, isSingleGroup);
        }

      } else if (ds.getType() == GribCollectionImmutable.Type.Best) {

        if (config.gribConfig.hasDatasetType(FeatureCollectionConfig.GribDatasetType.Best)) {
          DatasetBuilder best = new DatasetBuilder(result);
          best.setName(getDatasetNameBest(result.getName()));
          String path = pathStart + "/" + BEST_DATASET;
          // best.put(Dataset.UrlPath, path);
          best.addToList(Dataset.Documentation, new Documentation(null, null, null, "summary", "Single time dimension: for each forecast time, use GRIB record with smallest offset from reference time"));
          result.addDataset(best);

          makeDatasetsFromGroups(catURI, best, path, groups, isSingleGroup);
        }

      } else if (ds.getType() == GribCollectionImmutable.Type.MRUTP) {

        DatasetBuilder tp = new DatasetBuilder(result);
        tp.setName(getDatasetNameTP(result.getName()));
        String path = pathStart + "/" + PARTITION_DATASET;
        // tp.put(Dataset.UrlPath, path);
        tp.addToList(Dataset.Documentation, new Documentation(null, null, null, "summary", "Multiple reference, unique time Grib Partition"));
        result.addDataset(tp);

        makeDatasetsFromGroups(catURI, tp, path, groups, isSingleGroup);

      } else { // not a partition: SRC, MRC, MRUTC

        // is it a file partition with only one file?
        boolean isFilePartition = (config.ptype == FeatureCollectionConfig.PartitionType.file);
        boolean onlyOneFile = isFilePartition && fromGc.getFiles().size() == 1;
        if (onlyOneFile) {
          result.addServiceToCatalog(orgService);
          tmi.set(Dataset.ServiceName, this.orgService.getName());
          MFile mfile = fromGc.getFile(0);
          result.put(Dataset.DataSize, mfile.getLength());
          if (mfile.getLastModified() > 0) {
            CalendarDate cdate = CalendarDate.of(mfile.getLastModified());
            result.put(Dataset.Dates, new DateType(cdate).setType("modified"));
          }
        }

        String path = pathStart; //  + "/" + COLLECTION_DATASET;
        // result.put(Dataset.UrlPath, path);

        if (ds.getType() == GribCollectionImmutable.Type.SRC) {
          CoordinateRuntime runCoord = fromGc.getMasterRuntime();
          assert runCoord.getSize() == 1;
          CalendarDate runtime = runCoord.getFirstDate();
          result.addToList(Dataset.Documentation, new Documentation(null, null, null, "summary", "Single reference time Grib Collection"));
          result.addToList(Dataset.Documentation, new Documentation(null, null, null, "Reference Time", runtime.toString()));

        } else if (ds.getType() == GribCollectionImmutable.Type.MRC) {
          result.addToList(Dataset.Documentation, new Documentation(null, null, null, "summary", "Multiple reference time Grib Collection"));

        } else if (ds.getType() == GribCollectionImmutable.Type.MRUTC) {
          result.addToList(Dataset.Documentation, new Documentation(null, null, null, "summary", "Multiple reference, unique time Grib Collection"));

        } else {
          throw new IllegalStateException("Grib Collection '" + getName() + "' has illegal type " + ds.getType());
        }

        makeDatasetsFromGroups(catURI, result, path, groups, isSingleGroup);

        if (!onlyOneFile && config.gribConfig.hasDatasetType(FeatureCollectionConfig.GribDatasetType.Files)) {
          addFileDatasets(result, path, fromGc); // , config.gribConfig.hasDatasetType(FeatureCollectionConfig.GribDatasetType.Latest));
        }
      }

    }

    if (fromGc instanceof PartitionCollectionImmutable) {
      if (config.gribConfig.hasDatasetType(FeatureCollectionConfig.GribDatasetType.Latest) && parentCollectionName == null) {  // latest for top only
        DatasetBuilder ds = new DatasetBuilder(result);
        ds.setName(getDatasetNameLatest(result.getName()));
        ds.put(Dataset.UrlPath, LATEST_DATASET_CATALOG);
        ds.put(Dataset.Id, LATEST_DATASET_CATALOG);
        ds.addServiceToCatalog(latestService);
        ds.put(Dataset.ServiceName, latestService.getName());
        result.addDataset(ds);
      }

      PartitionCollectionImmutable pc = (PartitionCollectionImmutable) fromGc;
      for (PartitionCollectionImmutable.Partition partition : pc.getPartitionsSorted()) {
        DatasetBuilder partDs = makeDatasetFromPartition(result, parentCollectionName, partition, pc.isPartitionOfPartitions());
        result.addDataset(partDs);
      }
    }

    return result;
  }

  private void makeDatasetsFromGroups(URI catURI, DatasetBuilder parent, String parentPath, Iterable<GribCollectionImmutable.GroupGC> groups, boolean isSingleGroup) {

    for (GribCollectionImmutable.GroupGC group : groups) {
      DatasetBuilder ds;
      String dpath;
      if (isSingleGroup) {
        ds = parent;
        dpath = parentPath;
        ds.put(Dataset.Id, parentPath);
        ds.put(Dataset.UrlPath, parentPath);

      } else {
        ds = new DatasetBuilder(parent);
        ds.setName(group.getDescription());

        dpath = parentPath + "/" + group.getId();
        ds.put(Dataset.Id, dpath);
        ds.put(Dataset.UrlPath, dpath);

        // remove the urlPath on the parent if multiple groups;
        // cannot get a dataset with multiple groups in it
        //parent.put(Dataset.UrlPath, null);
        parent.addDataset(ds);
      }

      ThreddsMetadata tmi = ds.getInheritableMetadata();
      tmi.set(Dataset.GeospatialCoverage, extractGeospatial(group));
      tmi.set(Dataset.TimeCoverage, new DateRange(group.makeCalendarDateRange()));
      tmi.set(Dataset.VariableMapLinkURI, makeUriResolved(catURI, makeMetadataLink(dpath, VARIABLES)));
    }
  }

  private DatasetBuilder makeDatasetFromPartition(DatasetBuilder parent, String parentCollectionName, PartitionCollectionImmutable.Partition partition, boolean isPofP) throws IOException {
    String dsName = makeCollectionShortName(partition.getName());

    String startPath = parentCollectionName == null ? config.path : config.path + "/" + parentCollectionName;
    String dpath = startPath + "/" + partition.getName();
    CatalogRefBuilder result = new CatalogRefBuilder(parent);
    result.setTitle(dsName);
    result.setHref(buildCatalogServiceHref(dpath));  // LOOK could be plain if not PoP ??
    result.put(Dataset.Id, dpath);
    result.put(Dataset.UrlPath, dpath);
    result.addServiceToCatalog(virtualService);
    result.put(Dataset.ServiceName, virtualService.getName());

    return result;
  }

  /* file datasets of the partition catalog are InvCatalogRef pointing to "FileCatalogs"
  private void addFileDatasets(Dataset parent, String prefix) {
    String name = (prefix == null) ? FILES : prefix + "/" + FILES;
    InvCatalogRef ds = new InvCatalogRef(this, FILES, getCatalogHref(name));
    ds.finish();
    parent.addDataset(ds);
  }  */

  // this catalog lists the individual files comprising the collection.
  protected void addFileDatasets(DatasetBuilder parent, String parentPath, GribCollectionImmutable fromGc) throws IOException {

    DatasetBuilder filesParent = new DatasetBuilder(parent);
    filesParent.setName("Raw Files");
    filesParent.addServiceToCatalog(downloadService);
    ThreddsMetadata tmi = filesParent.getInheritableMetadata();
    tmi.set(Dataset.ServiceName, downloadService.getName());
    parent.addDataset(filesParent);

    List<MFile> mfiles = new ArrayList<>(fromGc.getFiles());
    Collections.sort(mfiles);

    // if not increasing (i.e. we WANT newest file listed first), reverse sort
    if (!this.config.getSortFilesAscending()) {
      Collections.reverse(mfiles);
    }

    for (MFile mfile : mfiles) {
      DatasetBuilder ds = new DatasetBuilder(parent);
      ds.setName(mfile.getName());
      String lpath = parentPath + "/" + FILES + "/" + mfile.getName();
      ds.put(Dataset.UrlPath, lpath);
      ds.put(Dataset.Id, lpath);
      ds.put(Dataset.DataSize, mfile.getLength());
      if (mfile.getLastModified() > 0) {
        CalendarDate cdate = CalendarDate.of(mfile.getLastModified());
        ds.put(Dataset.Dates, new DateType(cdate).setType("modified"));
      }
      filesParent.addDataset(ds);
    }
  }

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

  // called by DataRootHandler.makeDynamicCatalog() when a catref is requested
  // see top javadoc for possible URLs
  @Override
  public CatalogBuilder makeCatalog(String match, String reqPath, URI catURI) throws IOException {
    StateGrib localState = (StateGrib) checkState();
    if (localState == null) return null; // not ready yet maybe
    if (localState.gribCollection == null) return null; // not ready yet maybe

    try {

      // case 0
      if ((match == null) || (match.length() == 0)) {
        return makeCatalogTop(catURI, localState);  // top catalog : uses state.top previously made in checkState()
      }

      // case 1
      if (localState.gribCollection instanceof PartitionCollectionImmutable) {
        String[] paths = match.split("/");
        PartitionCollectionImmutable pc = (PartitionCollectionImmutable) localState.gribCollection;
        return makeCatalogFromPartition(pc, paths, 0, catURI);
      }

    } catch (Exception e) {
      e.printStackTrace();
      logger.error("Error making catalog for " + configPath, e);
    }

    return null;
  }

  private CatalogBuilder makeCatalogFromPartition(PartitionCollectionImmutable pc, String[] paths, int idx, URI catURI) throws IOException {
    if (paths.length < idx + 1) return null;
    PartitionCollectionImmutable.Partition pcp = pc.getPartitionByName(paths[idx]);
    if (pcp == null) return null;

    try (GribCollectionImmutable gc = pcp.getGribCollection()) {
      if (paths.length > idx + 1 && gc instanceof PartitionCollectionImmutable) {
        PartitionCollectionImmutable pcNested = (PartitionCollectionImmutable) gc;
        PartitionCollectionImmutable.Partition pcpNested = pcNested.getPartitionByName(paths[idx + 1]);
        if (pcpNested != null)  // recurse
          return makeCatalogFromPartition(pcNested, paths, idx + 1, catURI);
      }

      // build the parent name
      int i = 0;
      StringBuilder parentName = new StringBuilder();
      for (String s : paths) {
        if (i++ > 0) parentName.append("/");
        parentName.append(s);
      }
      return makeCatalogFromCollection(gc, parentName.toString(), catURI);
    }
  }

  private CatalogBuilder makeCatalogFromCollection(GribCollectionImmutable fromGc, String parentCollectionName, URI catURI) throws IOException { // }, URISyntaxException {
    Catalog parentCatalog = parent.getParentCatalog();

    CatalogBuilder result = new CatalogBuilder();
    result.setName(makeCollectionShortName(fromGc.getName()));
    result.setVersion(parentCatalog.getVersion());
    result.setBaseURI(catURI);

    DatasetBuilder ds = makeDatasetFromCollection(catURI, false, result, parentCollectionName, fromGc);
    result.addDataset(ds);

    return result;
  }

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

  @Override
  protected DatasetBuilder makeDatasetTop(URI catURI, State state) throws IOException {
    StateGrib localState = (StateGrib) state;
    return makeDatasetFromCollection(catURI, true, null, null, localState.gribCollection);
  }

  // path/latest.xml
  @Override
  public CatalogBuilder makeLatest(String matchPath, String reqPath, URI catURI) throws IOException {
    StateGrib localState = (StateGrib) checkState();
    if (!(localState.gribCollection instanceof PartitionCollectionImmutable)) return null;

    PartitionCollectionImmutable pc = (PartitionCollectionImmutable) localState.gribCollection;
    if (localState.latest == null) {
      List<String> paths = new ArrayList<>();
      GribCollectionImmutable latest = pc.getLatestGribCollection(paths);
      if (latest == null) return null;
      latest.close(); // doesnt need to be open

      // make pathname
      Formatter f = new Formatter();
      int count = 0;
      for (String p : paths) {
        if (count++ > 0) f.format("/");
        f.format("%s", p);
      }

      //synchronized (lock) {
      localState.latest = latest;
      localState.latestPath = f.toString();
      //}
    }

    return makeCatalogFromCollection(localState.latest, localState.latestPath, catURI);
  }

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

  // LOOK how come we arent using MetadataExtractor ??
  private ThreddsMetadata.GeospatialCoverage extractGeospatial(GribCollectionImmutable.GroupGC group) {
    GdsHorizCoordSys gdsCoordSys = group.getGdsHorizCoordSys();
    LatLonRect llbb = GridCoordSys.getLatLonBoundingBox(gdsCoordSys.proj, gdsCoordSys.getStartX(), gdsCoordSys.getStartY(),
            gdsCoordSys.getEndX(), gdsCoordSys.getEndY());

    double dx = 0.0, dy = 0.0;
    if (gdsCoordSys.isLatLon()) {
      dx = Math.abs(gdsCoordSys.dx);
      dy = Math.abs(gdsCoordSys.dy);
    }

    return new ThreddsMetadata.GeospatialCoverage(llbb, null, dx, dy);
  }

  private CalendarDateRange extractCalendarDateRange(Iterable<GribCollectionImmutable.GroupGC> groups) {
    CalendarDateRange gcAll = null;
    for (GribCollectionImmutable.GroupGC group : groups) {
      CalendarDateRange gc = group.makeCalendarDateRange();
      if (gcAll == null) gcAll = gc;
      else gcAll = gcAll.extend(gc);
    }
    return gcAll;
  }

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

  protected String getDatasetNameLatest(String dsName) {
    if (config.gribConfig != null && config.gribConfig.latestNamer != null) {
      return config.gribConfig.latestNamer;
    } else {
      return "Latest Collection for " + dsName;
    }
  }

  protected String getDatasetNameBest(String dsName) {
    if (config.gribConfig != null && config.gribConfig.bestNamer != null) {
      return config.gribConfig.bestNamer;
    } else {
      return "Best " + dsName + " Time Series";
    }
  }

  protected String getDatasetNameTP(String dsName) {
    return "Full Collection Dataset";
  }

  protected String getDatasetNameTwoD(String dsName) {
    return "Full Collection (Reference / Forecast Time) Dataset";
  }

  @Override
  public File getFile(String remaining) {
    try {
      StateGrib localState = (StateGrib) checkState();
      int pos = remaining.lastIndexOf("/");
      final String filename = (pos >= 0) && (remaining.length() > 1) ? remaining.substring(pos + 1) : remaining;

      MFile result = (MFile) findDataset(remaining, localState.gribCollection, new DatasetCreator() {
        @Override
        public Object obtain(GribCollectionImmutable gc, GribCollectionImmutable.Dataset ds, GribCollectionImmutable.GroupGC group) throws IOException {
          return gc.findMFileByName(filename);
        }
      });

      if (result == null) return null;
      return new File(result.getPath());

    } catch (IOException iow) {
      return null;
    }

  }

  public CoverageCollection getGridCoverage(String matchPath) throws IOException {
    StateGrib localState = (StateGrib) checkState();

    return (CoverageCollection) findDataset(matchPath, localState.gribCollection, new DatasetCreator() {
      @Override
      public Object obtain(GribCollectionImmutable gc, GribCollectionImmutable.Dataset ds, GribCollectionImmutable.GroupGC group) throws IOException {
        return gc.getGridCoverage(ds, group, null, config, null, logger);
      }
    });
  }

  @Override
  public ucar.nc2.dt.grid.GridDataset getGridDataset(String matchPath) throws IOException {
    StateGrib localState = (StateGrib) checkState();

    return (ucar.nc2.dt.grid.GridDataset) findDataset(matchPath, localState.gribCollection, new DatasetCreator() {
      @Override
      public Object obtain(GribCollectionImmutable gc, GribCollectionImmutable.Dataset ds, GribCollectionImmutable.GroupGC group) throws IOException {
        return gc.getGridDataset(ds, group, null, config, null, logger);
      }
    });
  }

  @Override
  public NetcdfDataset getNetcdfDataset(String matchPath) throws IOException {
    StateGrib localState = (StateGrib) checkState();

    return (NetcdfDataset) findDataset(matchPath, localState.gribCollection, new DatasetCreator() {
      @Override
      public Object obtain(GribCollectionImmutable gc, GribCollectionImmutable.Dataset ds, GribCollectionImmutable.GroupGC group) throws IOException {
        return gc.getNetcdfDataset(ds, group, null, config, null, logger);
      }
    });
  }

  // see top javadoc for possible URLs
  // returns visitor.obtain(), either a GridDataset or a NetcdfDataset
  private Object findDataset(String matchPath, GribCollectionImmutable topCollection, DatasetCreator visit) throws IOException {
    String[] paths = matchPath.split("/");
    List<String> pathList = (paths.length < 1) ? new ArrayList<>() : Arrays.asList(paths);
    DatasetAndGroup dg = findDatasetAndGroup(pathList, topCollection);
    if (dg != null)
      return visit.obtain(topCollection, dg.ds, dg.group);

    if (!(topCollection instanceof PartitionCollectionImmutable)) return null;
    PartitionCollectionImmutable pc = (PartitionCollectionImmutable) topCollection;
    return findDatasetPartition(visit, pc, pathList);
  }

  private static class DatasetAndGroup {
    GribCollectionImmutable.Dataset ds;
    GribCollectionImmutable.GroupGC group;

    private DatasetAndGroup(GribCollectionImmutable.Dataset ds, GribCollectionImmutable.GroupGC group) {
      this.ds = ds;
      this.group = group;
    }
  }

  /* case 0: path/dataset (dataset = "")              // single dataset
     case 1: path/dataset (dataset = BEST, TWOD, TP)  // single group
     case 2: path/dataset/groupName                  // dataset with group
     case 3: path/groupName                           // single dataset not sure this is actually used ??
  */
  private DatasetAndGroup findDatasetAndGroup(List<String> paths, GribCollectionImmutable gc) {
    if (paths.size() < 1 || paths.get(0).length() == 0) {   // case 0: use first dataset,group in the collection
      GribCollectionImmutable.Dataset ds = gc.getDataset(0);
      GribCollectionImmutable.GroupGC dg = ds.getGroup(0);
      return new DatasetAndGroup(ds, dg);
    }

    GribCollectionImmutable.Dataset ds = getSingleDatasetOrByTypeName(gc, paths.get(0));
    if (ds == null) return null;

    boolean isSingleGroup = ds.getGroupsSize() == 1;
    if (isSingleGroup) {
      GribCollectionImmutable.GroupGC g = ds.getGroup(0); // case 1
      return new DatasetAndGroup(ds, g);
    }

    // otherwise last component is group name
    String groupName = (paths.size() == 1) ? paths.get(0) : paths.get(1); // case 3 else case 2
    GribCollectionImmutable.GroupGC g = ds.findGroupById(groupName);
    if (g != null)
      return new DatasetAndGroup(ds, g);

    return null;
  }

  // kinda kludgey, but trying not keep URLs stable
  public GribCollectionImmutable.Dataset getSingleDatasetOrByTypeName(GribCollectionImmutable gc, String typeName) {
    if (gc.getDatasets().size() == 1) return gc.getDataset(0);
    for (GribCollectionImmutable.Dataset ds : gc.getDatasets())
      if (ds.getType().toString().equalsIgnoreCase(typeName)) return ds;
    return null;
  }

  private Object findDatasetPartition(DatasetCreator visit, PartitionCollectionImmutable outerPartition, List<String> pathList) throws IOException {
    int n = pathList.size();
    if (pathList.size() < 1) return null;
    PartitionCollectionImmutable.Partition pcp = outerPartition.getPartitionByName(pathList.get(0));
    if (pcp == null) {
      DatasetAndGroup dg = findDatasetAndGroup(pathList, outerPartition);
      if (dg != null)
        return visit.obtain(outerPartition, dg.ds, dg.group);
      else
        return null;
    }

    try (GribCollectionImmutable gc = pcp.getGribCollection()) {
      if (gc instanceof PartitionCollectionImmutable) {
        PartitionCollectionImmutable pcNested = (PartitionCollectionImmutable) gc;
        return findDatasetPartition(visit, pcNested, pathList.subList(1, n));
      }

      DatasetAndGroup dg = findDatasetAndGroup(pathList.subList(1, n), gc);
      if (dg != null)
        return visit.obtain(gc, dg.ds, dg.group);
      else {
        GribCollectionImmutable.Dataset ds = gc.getDataset(0);
        GribCollectionImmutable.GroupGC group = ds.getGroup(0);
        return visit.obtain(gc, ds, group);
      }
    }
  }

  private interface DatasetCreator {  // Visitor pattern
    Object obtain(GribCollectionImmutable gc, GribCollectionImmutable.Dataset ds, GribCollectionImmutable.GroupGC group) throws IOException;
  }

}


package ucar.nc2.grib.coord;

import java.util.Formatter;
import java.util.Objects;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.Immutable;
import ucar.nc2.grib.GribNumbers;
import ucar.nc2.util.Misc;

@Immutable
public class VertCoordValue implements Comparable<VertCoordValue> {
  private final double value1;
  private final double value2;
  private final double mid;
  private final boolean isLayer;

  public VertCoordValue(double value1) {
    this.value1 = value1;
    this.value2 = GribNumbers.UNDEFINEDD;
    this.mid = value1;
    this.isLayer = false;
  }

  public VertCoordValue(double value1, double value2) {
    this.value1 = value1;
    this.value2 = value2;
    this.mid = (Double.compare(value2, 0.0) == 0 || GribNumbers.isUndefined(value2)) ? value1 : (value1 + value2) / 2;
    this.isLayer = true;
  }

  public double getValue1() {
    return value1;
  }

  public double getValue2() {
    return value2;
  }

  public double getMid() {
    return mid;
  }

  public boolean isLayer() {
    return isLayer;
  }

  @Override
  public int compareTo(@Nonnull VertCoordValue o) {
    return Double.compare(mid, o.mid);
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    VertCoordValue that = (VertCoordValue) o;
    return Double.compare(that.value1, value1) == 0 &&
        Double.compare(that.value2, value2) == 0 &&
        Double.compare(that.mid, mid) == 0 &&
        isLayer == that.isLayer;
  }

  @Override
  public int hashCode() {
    return Objects.hash(value1, value2, mid, isLayer);
  }

  // cannot do approx equals and be consistent with hashCode, so make seperate call
  public boolean nearlyEquals(VertCoordValue other) {
    return Misc.nearlyEquals(value1, other.value1) && Misc.nearlyEquals(value2, other.value2);
  }

  public String toString() {
    try (Formatter out = new Formatter()) {
      if (isLayer) {
        out.format("(%f,%f)", value1, value2);
      } else {
        out.format("%f", value1);
      }
      return out.toString();
    }
  }

  public String toString(boolean isLayer) {
    try (Formatter out = new Formatter()) {
      if (isLayer) {
        out.format("(%f,%f)", value1, value2);
      } else {
        out.format("%f", value1);
      }
      return out.toString();
    }
  }
}


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

import java.io.Serializable;

/**
 * Provides support for unit identifiers.
 *
 * @author Steven R. Emmerson
 */
public abstract class UnitID implements Serializable {
	private static final long	serialVersionUID	= 1L;

	/**
	 * Factory method for constructing an identifier from a name, plural, and
	 * symbol.
	 *
	 * @param name
	 *            The name for the unit. May be null
     *				.
	 * @param plural
	 *            The plural form of the name. If null
     *				 and
	 *            name is non-
     *				null, then regular
	 *            plural-forming rules are used on the name.
	 * @param symbol
	 *            The symbol for the unit. May be null
     *				.
	 */
	public static UnitID newUnitID(final String name, final String plural,
			final String symbol) {
		UnitID id;
		try {
			id = name == null
					? new UnitSymbol(symbol)
					: UnitName.newUnitName(name, plural, symbol);
		}
		catch (final NameException e) {
			id = null; // can't happen
		}
		return id;
	}

	/**
	 * Returns the name of the unit.
	 *
	 * @return The name of the unit. May be null.
	 */
	public abstract String getName();

	/**
	 * Returns the plural form of the name of the unit.
	 *
	 * @return The plural form of the name of the unit. May be null
	 *         .
	 */
	public abstract String getPlural();

	/**
	 * Returns the symbol for the unit.
	 *
	 * @return The symbol for the unit. May be null.
	 */
	public abstract String getSymbol();

	/**
	 * Returns the string representation of this identifier.
	 *
	 * @return The string representation of this identifier.
	 */
	@Override
	public abstract String toString();
}


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

package ucar.nc2.iosp.grads;

/**
 * Hold information about GrADS attributes
 * @author         Don Murray - CU/CIRES
 */
public class GradsAttribute {

    /** the global identifier */
    public static final String GLOBAL = "global";

    /** Grads String identifier */
    public static final String STRING = "String";

    /** Grads Btye identifier */
    public static final String BYTE = "Byte";

    /** GrADS Int16 identifier */
    public static final String INT16 = "Int16";

    /** GrADS UInt16 identifier */
    public static final String UINT16 = "UInt16";

    /** GrADS Int32 identifier */
    public static final String INT32 = "Int32";

    /** GrADS UInt32 identifier */
    public static final String UINT32 = "UInt32";

    /** GrADS Float32 identifier */
    public static final String FLOAT32 = "Float32";

    /** GrADS Float64 identifier */
    public static final String FLOAT64 = "Float64";

    /** variable name */
    private String varName;

    /** attribute type */
    private String type;

    /** attribute name */
    private String attrName;

    /** attribute value */
    private String attrValue;

    /**
     * Create a GradsAttribute
     *
     * @param vName  the variable
     * @param aType  the attribute type
     * @param aName  the attribute name
     * @param aValue  the attribute value
     */
    public GradsAttribute(String vName, String aType, String aName,
                          String aValue) {
        this.varName   = vName;
        this.type      = aType;
        this.attrName  = aName;
        this.attrValue = aValue;
    }

    /**
     * Parse an attribute spec
     *
     * @param attrSpec  the attribute spec (e.g. @ precip String units mm/day)
     *
     * @return the associated attribute
     */
    public static GradsAttribute parseAttribute(String attrSpec) {
        String[]     toks = attrSpec.split("\\s+");
        StringBuffer buf  = new StringBuffer();
        for (int i = 4; i < toks.length; i++) {
            buf.append(toks[i]);
            buf.append(" ");
        }

        // toks[0] is "@"
        return new GradsAttribute(toks[1], toks[2], toks[3],
                                  buf.toString().trim());
    }

    /**
     * Get the variable this is associated with
     *
     * @return  the variable name or GLOBAL
     */
    public String getVariable() {
        return varName;
    }

    /**
     * @return the type
     */
    public String getType() {
        return type;
    }

    /**
     * @return the attrName
     */
    public String getName() {
        return attrName;
    }

    /**
     * @return the attrValue
     */
    public String getValue() {
        return attrValue;
    }

}


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

package ucar.nc2.iosp.nowrad;

//~--- non-JDK imports --------------------------------------------------------

import ucar.ma2.Array;
import ucar.ma2.DataType;
import ucar.nc2.Attribute;
import ucar.nc2.Dimension;
import ucar.nc2.Variable;
import ucar.nc2.constants.AxisType;
import ucar.nc2.constants.CDM;
import ucar.nc2.constants.FeatureType;
import ucar.nc2.constants._Coordinate;
import ucar.nc2.units.DateFormatter;
import ucar.unidata.geoloc.LatLonPointImpl;
import ucar.unidata.geoloc.ProjectionImpl;
import ucar.unidata.geoloc.ProjectionPointImpl;
import ucar.unidata.geoloc.projection.LambertConformal;
import ucar.unidata.util.Parameter;
import ucar.unidata.util.StringUtil2;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;

/**
 * Created by IntelliJ IDEA.
 * User: yuanho
 * Date: Feb 10, 2010
 * Time: 11:21:19 AM
 * To change this template use File | Settings | File Templates.
 */
public class NOWRadheader {
    final static int                NEXET        = 2;        // Echo Tops Composite
    final static int                NEXLH        = 5;        // Layer Reflectivity - High
    final static int                NEXLL        = 3;        // Layer Reflectivity - Low
    final static int                NEXLM        = 4;        // Layer Reflectivity - Mid
    final static int                NEXVI        = 6;        // Vert. Integrated Liquid Water
    final static int                NOWRADHF     = 0;        // 2km Base Reflectivity
    final static int                USRADHF      = 1;        // 8km Base Reflectivity
    static public String            mons[]       = {
        "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"
    };
    static private org.slf4j.Logger log          = org.slf4j.LoggerFactory.getLogger(NOWRadheader.class);
    DateFormatter                   formatter    = new DateFormatter();

    // private PrintStream out = System.out;
    // private Vinfo myInfo;
    private String              cmemo, ctilt, ctitle, cunit, cname;
    private ucar.nc2.NetcdfFile ncfile;

    // message header block
    // production dessciption block
    private int                              numX;
    private int                              numY;
    ucar.unidata.io.RandomAccessFile raf;

    /**
     * check if this file is a nids / tdwr file
     * @param raf    input file
     * @return  true  if valid
     */
    public boolean isValidFile(ucar.unidata.io.RandomAccessFile raf) {
        try {
            long t = raf.length();

            if (t == 0) {
                throw new IOException("zero length file ");
            }

        } catch (IOException e) {
            return (false);
        }

        try {
            int p = this.readTop(raf);

            if (p == 0) {
                return false;
            }
        } catch (IOException e) {
            return (false);
        }

        return true;
    }

    /**
     * read the header of input file and parsing the NOWRAD part
     * @param raf    input file
     * @return        1 if checking passing
     * @throws IOException
     */
    int readTop(ucar.unidata.io.RandomAccessFile raf) throws IOException {
        int pos = 0;

        // long     actualSize = 0;
        raf.seek(pos);

        int readLen = 35;

        // Read in the contents of the NEXRAD Level III product head
        byte[] b = new byte[readLen];

        int rc = raf.read(b);

        if (rc != readLen) {
            return 0;
        }

        // check
        if ((convertunsignedByte2Short(b[0]) != 0x00) || (convertunsignedByte2Short(b[1]) != 0xF0)
                || (convertunsignedByte2Short(b[2]) != 0x09)) {
            return 0;
        }

        String pidd  = new String(b, 15, 5, CDM.utf8Charset);

        if (pidd.contains("NOWRA") || pidd.contains("USRAD") || pidd.contains("NEX")) {
            return 1;
        } else {
            return 0;
        }
    }

    public byte[] getData(int offset) throws IOException {
        int    readLen = (int) raf.length();
        byte[] b       = new byte[readLen];
        int    pos     = 0;

        raf.seek(pos);
        raf.readFully(b);

        return b;
    }

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

    public void setProperty(String name, String value) {}

    /**
     * read and parse the header of the nids/tdwr file
     * @param raf       input file
     * @param ncfile    output file
     * @throws IOException
     */
    void read(ucar.unidata.io.RandomAccessFile raf, ucar.nc2.NetcdfFile ncfile) throws Exception {
        this.raf = raf;

        int rc;    /* function return status */
        int hoffset;
        int readLen = 250;

        this.ncfile = ncfile;

        int pos = 0;

        raf.seek(pos);

        byte[] b = new byte[readLen];

        rc = raf.read(b);

        if (rc != readLen) {
            log.warn(" error reading nids product header " + raf.getLocation());
        }

        int    hsize   = b[3];
        String product = new String(b, 15, 8, CDM.utf8Charset);

        // image lines
        //byte[] bt   = new byte[] { (byte) 0xF0, (byte) 0x0A };
        int    t1 = 0  ;
        int ii = 0;
        for(int i = 0; i < readLen; i++ ){
            if(convertunsignedByte2Short(b[i+hsize]) == 0xF0 &&
                     convertunsignedByte2Short(b[i+1+hsize]) == 0x0A ){
                t1 = i + hsize;
                ii = i;
                break;
            }
        }
        if(t1 == 0)
            return;
        // if(convertunsignedByte2Short(b[6+hsize]) != 0xF0 ||
        // convertunsignedByte2Short(b[7+hsize]) != 0x0A )
        // return;
        String lstr = trim(new String(b, t1 + 2, 4, CDM.utf8Charset));

        numY = Integer.parseInt(lstr);

        String estr = trim(new String(b, t1 + 6, 5, CDM.utf8Charset));

        numX = Integer.parseInt(estr);
        //bt   = new byte[] { (byte) 0xF0, (byte) 0x03 };

        t1   = 0;
        for(int i = ii; i < readLen; i++ ){
            if(convertunsignedByte2Short(b[i+hsize]) == 0xF0 &&
                    convertunsignedByte2Short(b[i+1+hsize]) == 0x03 ){
                t1 = i + hsize;
                ii = i;
                break;
            }
        }
        if(t1 == 0)
            return;
        // if((lstr.length()+estr.length() < 8))
        // hsize = hsize -2;

        // if(convertunsignedByte2Short(b[18+hsize]) != 0xF0 ||
        // convertunsignedByte2Short(b[19+hsize]) != 0x03 )
        // return;
        int    off = 0;

        if (product.contains("USRADHF")) {
            off = 3;
        }

        // Image time, HHMMSS.  The time will be in the form HH:MM, so look :
        String ts = new String(b, t1 + 22 + off, 2, CDM.utf8Charset);
        int    hr = Integer.parseInt(ts);

        ts = new String(b, t1 + 25 + off, 2, CDM.utf8Charset);

        int min = Integer.parseInt(ts);

        ts = new String(b, t1 + 28 + off, 2, CDM.utf8Charset);

        int dd = Integer.parseInt(ts);

        ts = new String(b, t1 + 31 + off, 3, CDM.utf8Charset);

        String mon   = ts;
        int    month = getMonth(mon);

        ts = new String(b, t1 + 35 + off, 2, CDM.utf8Charset);

        int              year = Integer.parseInt(ts);
        SimpleDateFormat sdf  = new SimpleDateFormat();

        sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
        sdf.applyPattern("yyyy/MM/dd HH:mm");

        Date   date = sdf.parse(year + "/" + month + "/" + dd + " " + hr + ":" + min);

        //bt = new byte[] { (byte) 0xF0, (byte) 0x0b };
        t1 = 0;

        for(int i = ii; i < readLen; i++ ){
            if(convertunsignedByte2Short(b[i+hsize]) == 0xF0 &&
                    convertunsignedByte2Short(b[i+1+hsize]) == 0x0b ) {
                t1 = i + hsize;
                break;
            }
        }
        if(t1 == 0)
            return;
        // if( convertunsignedByte2Short(b[101 + hsize]) != 0xF0 ||
        // convertunsignedByte2Short(b[102 + hsize]) != 0x0b )
        // return;
        if (product.contains("NOWRAD")) {
            String ot = new String(b, t1 + 2, 68, CDM.utf8Charset);

            //List<String> toks = StringUtil.split(ot, " ", true, true);
            String[] toks = StringUtil2.splitString(ot);
            double       nav1 = Math.toDegrees(Double.parseDouble(toks[1]));    // lon
            double       nav2 = Math.toDegrees(Double.parseDouble(toks[2]));    // lat
            double       nav3 = Math.toDegrees(Double.parseDouble(toks[3]));
            double       nav4 = Math.toDegrees(Double.parseDouble(toks[4]));    // lat sp
            double       nav5 = Math.toDegrees(Double.parseDouble(toks[5]));    // lon sp

            // lower left and upper right corner
            float rlat1;
            float rlon1;
            float rlat2;
            float rlon2;

            rlat1   = (float) (nav2 - (numY - 1) * nav4);
            rlon1   = (float) (nav1 + nav3);
            rlat2   = (float) nav2;
            rlon2   = (float) (nav1 - nav3);
            hoffset = t1 + 71;    // 172 + hsize;

            // start of the image sequence
            if ((convertunsignedByte2Short(b[172 + hsize]) != 0xF0)
                    || (convertunsignedByte2Short(b[173 + hsize]) != 0x0c)) {
                return;
            }

            // hoffset = 174 + hsize;
            // Set product-dependent information
            setProductInfo(product, date);

            // data struct
            nowrad(hoffset, rlat1, rlon1, rlat2, rlon2, (float) nav4, (float) nav5, date);
        } else if (product.contains("USRADHF")) {
            String ot = new String(b, t1 + 2, 107, CDM.utf8Charset);

            String[] toks = StringUtil2.splitString(ot);
            double       nav1 = Math.toDegrees(Double.parseDouble(toks[1]));    // standard lat 1
            double       nav2 = Math.toDegrees(Double.parseDouble(toks[2]));    // standard lat 2
            double       nav3 = Math.toDegrees(Double.parseDouble(toks[3]));    // lat. center of proj
            double       nav4 = Math.toDegrees(Double.parseDouble(toks[4]));    // lon. center of proj
            double       nav5 = Math.toDegrees(Double.parseDouble(toks[5]));    // upper left lat
            double       nav6 = Math.toDegrees(Double.parseDouble(toks[6]));    // upper left lon

            /* List<String> toks = StringUtil.split(ot, " ", true, true);
            String       pj   = toks.get(0);
            double       nav1 = Math.toDegrees(Double.parseDouble(toks.get(1)));    // standard lat 1
            double       nav2 = Math.toDegrees(Double.parseDouble(toks.get(2)));    // standard lat 2
            double       nav3 = Math.toDegrees(Double.parseDouble(toks.get(3)));    // lat. center of proj
            double       nav4 = Math.toDegrees(Double.parseDouble(toks.get(4)));    // lon. center of proj
            double       nav5 = Math.toDegrees(Double.parseDouble(toks.get(5)));    // upper left lat
            double       nav6 = Math.toDegrees(Double.parseDouble(toks.get(6)));    // upper left lon
            double       nav7 = Math.toDegrees(Double.parseDouble(toks.get(7)));    // lat sp
            double       nav8 = Math.toDegrees(Double.parseDouble(toks.get(8)));    // lon sp  */

            // lower left and upper right corner
            // int offh = 39;
            hoffset = t1 + 110;    // 172 + hsize+ offh;

            // start of the image sequence
            if ((convertunsignedByte2Short(b[t1 + 110]) != 0xF0) || (convertunsignedByte2Short(b[t1 + 111]) != 0x0c)) {
                return;
            }

            // Set product-dependent information
            setProductInfo(product, date);

            // data struct
            nowradL(hoffset, (float) nav1, (float) nav2, (float) nav3, (float) nav4, (float) nav5, (float) nav6, date);
        }

        ncfile.finish();
    }

    String trim(String str) {
        int          len  = str.length();
        StringBuilder ostr = new StringBuilder();

        for (int i = 0; i < len; i++) {
            char sc = str.charAt(i);

            if (Character.isDigit(sc)) {
                ostr.append(sc);
            }
        }

        return ostr.toString();
    }

    int getMonth(String m) {
        int i = 0;

        while (i < 12) {
            if (m.equalsIgnoreCase(mons[i])) {
                return i + 1;
            } else {
                i++;
            }
        }

        return 0;
    }

    ProjectionImpl  nowradL(int hoff, float lat1, float lat2, float clat, float clon, float lat, float lon, Date dd) {
        List<Dimension> dims = new ArrayList<>();
        Dimension dimT = new Dimension("time", 1, true, false, false);

        ncfile.addDimension(null, dimT);

        String   timeCoordName = "time";
        Variable taxis         = new Variable(ncfile, null, null, timeCoordName);

        taxis.setDataType(DataType.DOUBLE);
        taxis.setDimensions("time");
        taxis.addAttribute(new Attribute(CDM.LONG_NAME, "time since base date"));
        taxis.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Time.toString()));

        double[] tdata = new double[1];

        tdata[0] = dd.getTime();

        Array dataT = Array.factory(DataType.DOUBLE, new int[] { 1 }, tdata);

        taxis.setCachedData(dataT, false);

        DateFormatter formatter = new DateFormatter();

        taxis.addAttribute(new Attribute(CDM.UNITS, "msecs since " + formatter.toDateTimeStringISO(new Date(0))));
        ncfile.addVariable(null, taxis);
        dims.add(dimT);

        Dimension jDim = new Dimension("y", numY, true, false, false);
        Dimension iDim = new Dimension("x", numX, true, false, false);

        dims.add(jDim);
        dims.add(iDim);
        ncfile.addDimension(null, iDim);
        ncfile.addDimension(null, jDim);
        ncfile.addAttribute(null, new Attribute("cdm_data_type", FeatureType.GRID.toString()));

        String   coordinates = "time y x";
        Variable v           = new Variable(ncfile, null, null, cname);

        v.setDataType(DataType.BYTE);
        v.setDimensions(dims);
        ncfile.addVariable(null, v);
        v.addAttribute(new Attribute(CDM.LONG_NAME, ctitle));
        v.addAttribute(new Attribute(CDM.UNITS, cunit));
        v.addAttribute(new Attribute(CDM.SCALE_FACTOR, 5.0f));
        v.addAttribute(new Attribute(CDM.MISSING_VALUE, 0));
        v.setSPobject(new Vinfo(numX, numY, hoff));
        v.addAttribute(new Attribute(_Coordinate.Axes, coordinates));

        // create coordinate variables
        Variable xaxis = new Variable(ncfile, null, null, "x");

        xaxis.setDataType(DataType.DOUBLE);
        xaxis.setDimensions("x");
        xaxis.addAttribute(new Attribute("standard_name", "projection x coordinate"));
        xaxis.addAttribute(new Attribute(CDM.UNITS, "km"));
        xaxis.addAttribute(new Attribute(_Coordinate.AxisType, "GeoX"));

        double[]       data1      = new double[numX];
        ProjectionImpl projection = new LambertConformal(clat, clon, lat1, lat2);
        double ullat = 51.8294;
        double ullon = -135.8736;
        double lrlat = 17.2454;
        double lrlon = -70.1154;

        ProjectionPointImpl ptul = (ProjectionPointImpl) projection.latLonToProj(new LatLonPointImpl(ullat, ullon));
        ProjectionPointImpl ptlr = (ProjectionPointImpl) projection.latLonToProj(new LatLonPointImpl(lrlat, lrlon));
        double startX = ptul.getX();
        double startY = ptlr.getY();
        double dx = (ptlr.getX() - ptul.getX())/(numX-1);
        for (int i = 0; i < numX; i++) {
            data1[i] = startX + i*dx;
        }

        Array dataA = Array.factory(DataType.DOUBLE, new int[] { numX }, data1);

        xaxis.setCachedData(dataA, false);
        ncfile.addVariable(null, xaxis);

        Variable yaxis = new Variable(ncfile, null, null, "y");

        yaxis.setDataType(DataType.DOUBLE);
        yaxis.setDimensions("y");
        yaxis.addAttribute(new Attribute("standard_name", "projection y coordinate"));
        yaxis.addAttribute(new Attribute(CDM.UNITS, "km"));
        yaxis.addAttribute(new Attribute(_Coordinate.AxisType, "GeoY"));
        data1 = new double[numY];
        double dy = (ptul.getY() - ptlr.getY())/(numY-1);

        for (int i = 0; i < numY; i++) {
            data1[i] = startY + i*dy;
        }

        dataA = Array.factory(DataType.DOUBLE, new int[] { numY }, data1);
        yaxis.setCachedData(dataA, false);
        ncfile.addVariable(null, yaxis);

        // projection
        // lower left and upper right corner lat/lons
        // modified cylind. equidistant or  CED with lat/lon ration != 1
        Variable ct = new Variable(ncfile, null, null, projection.getClassName());

        ct.setDataType(DataType.CHAR);
        ct.setDimensions("");

        List<Parameter> params = projection.getProjectionParameters();

        for (Parameter p : params) {
            ct.addAttribute(new Attribute(p));
        }

        ct.addAttribute(new Attribute(_Coordinate.TransformType, "Projection"));
        //ct.addAttribute(new Attribute(_Coordinate.Axes, "lat lon"));
        ct.addAttribute( new Attribute(_Coordinate.Axes, "x y "));
        // fake data
        dataA = Array.factory(DataType.CHAR, new int[] {});
        dataA.setChar(dataA.getIndex(), ' ');
        ct.setCachedData(dataA, false);
        ncfile.addVariable(null, ct);

        return projection;
    }

    /**
     *  construct a raster dataset for NIDS raster products;
     *
     * @return  soff -- not used
     */
    ProjectionImpl  nowrad(int hoff, float rlat1, float rlon1, float rlat2, float rlon2, float dlat, float dlon, Date dd) {
        List<Dimension> dims = new ArrayList<>();
        Dimension dimT = new Dimension("time", 1, true, false, false);

        ncfile.addDimension(null, dimT);

        String   timeCoordName = "time";
        Variable taxis         = new Variable(ncfile, null, null, timeCoordName);

        taxis.setDataType(DataType.DOUBLE);
        taxis.setDimensions("time");
        taxis.addAttribute(new Attribute(CDM.LONG_NAME, "time since base date"));
        taxis.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Time.toString()));

        double[] tdata = new double[1];

        tdata[0] = dd.getTime();

        Array dataT = Array.factory(DataType.DOUBLE, new int[] { 1 }, tdata);

        taxis.setCachedData(dataT, false);

        DateFormatter formatter = new DateFormatter();

        taxis.addAttribute(new Attribute(CDM.UNITS, "msecs since " + formatter.toDateTimeStringISO(new Date(0))));
        ncfile.addVariable(null, taxis);
        dims.add(dimT);

        Dimension jDim = new Dimension("lat", numY, true, false, false);
        Dimension iDim = new Dimension("lon", numX, true, false, false);

        dims.add(jDim);
        dims.add(iDim);
        ncfile.addDimension(null, iDim);
        ncfile.addDimension(null, jDim);
        ncfile.addAttribute(null, new Attribute("cdm_data_type", FeatureType.GRID.toString()));

        String   coordinates = "time lat lon";
        Variable v           = new Variable(ncfile, null, null, cname);

        v.setDataType(DataType.BYTE);
        v.setDimensions(dims);
        ncfile.addVariable(null, v);
        v.addAttribute(new Attribute(CDM.LONG_NAME, ctitle));
        v.addAttribute(new Attribute(CDM.SCALE_FACTOR, 5.0f));
        v.addAttribute(new Attribute(CDM.MISSING_VALUE, 0));
        v.addAttribute(new Attribute(CDM.UNITS, cunit));
        v.setSPobject(new Vinfo(numX, numY, hoff));
        v.addAttribute(new Attribute(_Coordinate.Axes, coordinates));

        // create coordinate variables
        Variable xaxis = new Variable(ncfile, null, null, "lon");
        xaxis.setDataType(DataType.DOUBLE);
        xaxis.setDimensions("lon");
        xaxis.addAttribute(new Attribute(CDM.LONG_NAME, "longitude"));
        xaxis.addAttribute(new Attribute(CDM.UNITS, "degree"));
        xaxis.addAttribute(new Attribute(_Coordinate.AxisType, "Lon"));

        double[] data1 = new double[numX];

        for (int i = 0; i < numX; i++) {
            data1[i] = (double) (rlon1 + i * dlon);
        }

        Array dataA = Array.factory(DataType.DOUBLE, new int[] { numX }, data1);

        xaxis.setCachedData(dataA, false);
        ncfile.addVariable(null, xaxis);

        Variable yaxis = new Variable(ncfile, null, null, "lat");
        yaxis.setDataType(DataType.DOUBLE);
        yaxis.setDimensions("lat");
        yaxis.addAttribute(new Attribute(CDM.LONG_NAME, "latitude"));
        yaxis.addAttribute(new Attribute(CDM.UNITS, "degree"));
        yaxis.addAttribute(new Attribute(_Coordinate.AxisType, "Lat"));
        data1 = new double[numY];

        for (int i = 0; i < numY; i++) {
            data1[i] = rlat1 + i * dlat;
        }

        dataA = Array.factory(DataType.DOUBLE, new int[] { numY }, data1);
        yaxis.setCachedData(dataA, false);
        ncfile.addVariable(null, yaxis);

        // projection
        // lower left and upper right corner lat/lons
        // modified cylind. equidistant or  CED with lat/lon ration != 1
    /*    LatLonProjection llproj = new LatLonProjection("LatitudeLongitudeProjection",
                                  new ProjectionRect(rlat1, rlon1, rlat2, rlon2));
        Variable ct = new Variable(ncfile, null, null, llproj.getClassName());

        ct.setDataType(DataType.CHAR);
        ct.setDimensions("");

        List params = llproj.getProjectionParameters();

        for (int i = 0; i < params.size(); i++) {
            Parameter p = (Parameter) params.get(i);

            ct.addAttribute(new Attribute(p));
        }

        ct.addAttribute(new Attribute(_Coordinate.TransformType, "Projection"));
        ct.addAttribute(new Attribute(_Coordinate.Axes, "lat lon"));

        // fake data
        dataA = Array.factory(DataType.CHAR, new int[] {});
        dataA.setChar(dataA.getIndex(), ' ');
        ct.setCachedData(dataA, false);
        ncfile.addVariable(null, ct);
      */
        return null;
    }

    /**
     *  parsing the product information into netcdf dataset
     */
    void setProductInfo(String prod, Date dd) {
        String summary = null;

        if (prod.contains("NOWRADHF")) {
            cmemo   = "NOWRAD  Base Reflectivity at Tilt 1";
            ctitle  = "BREF: Base Reflectivity [dBZ]";
            cunit   = "dBZ";
            cname   = "Reflectivity";
            summary = "NOWRAD Product";
        } else if (prod.contains("USRADHF")) {
            cmemo   = "NOWRAD  Base Reflectivity at Tilt 1";
            ctitle  = "BREF: Base Reflectivity [dBZ]";
            cunit   = "dBZ";
            cname   = "Reflectivity";
            summary = "NOWRAD Product";
        } else if (prod.contains("NEXET")) {
            cmemo   = "NOWRAD Echo Tops";
            ctitle  = "Echo Tops Composite";
            cunit   = "K FT";
            cname   = "EchoTopsComposite";
            summary = "NOWRAD Product";
        } else if (prod.contains("NEXLL")) {
            cmemo   = "NOWRAD Layer Comp. Reflectivity - Low";
            ctitle  = "LayerReflectivityLow";
            cunit   = "dBZ";
            cname   = "Reflectivity";
            summary = "NOWRAD Product";
        } else if (prod.contains("NEXLM")) {
            cmemo   = "NOWRAD Layer Comp. Reflectivity - Mid";
            ctitle  = "LayerReflectivityMid";
            cunit   = "dBZ";
            cname   = "Reflectivity";
            summary = "NOWRAD Product";
        } else if (prod.contains("NEXLH")) {
            cmemo   = "NOWRAD Layer Comp. Reflectivity - High";
            ctitle  = "LayerReflectivityHigh";
            cunit   = "dBZ";
            cname   = "ReflectivityHigh";
            summary = "NOWRAD Product";
        } else if (prod.contains("NEXVI")) {
            cmemo   = "NOWRAD ";
            ctitle  = "Vert. Integrated Liquid Water";
            cunit   = "Knots";
            cname   = "VILwater";
            summary = "NOWRAD ";
        } else {
            ctilt  = "error";
            ctitle = "error";
            cunit  = "error";
            cname  = "error";
        }

        /* add geo global att */
        ncfile.addAttribute(null, new Attribute("summary", "NOWRAD radar composite products." + summary));
        ncfile.addAttribute(null, new Attribute("title", "NOWRAD"));
        ncfile.addAttribute(null, new Attribute("keywords", "NOWRAD"));
        ncfile.addAttribute(null, new Attribute("creator_name", "NOAA/NWS"));
        ncfile.addAttribute(null, new Attribute("creator_url", "http://www.ncdc.noaa.gov/oa/radar/radarproducts.html"));
        ncfile.addAttribute(null, new Attribute("naming_authority", "NOAA/NCDC"));
        ncfile.addAttribute(null, new Attribute("base_date", formatter.toDateOnlyString(dd)));
        ncfile.addAttribute(null, new Attribute("conventions", _Coordinate.Convention));
        ncfile.addAttribute(null, new Attribute("cdm_data_type", FeatureType.GRID.toString()));

    }

    /* thredds global att */

    /**
     * convert two short into a integer
     * @param s1            short one
     * @param s2            short two
     * @param swapBytes      if swap bytes
     * @return    integer
     */
    public static int shortsToInt(short s1, short s2, boolean swapBytes) {
        byte[] b = new byte[4];

        b[0] = (byte) (s1 >>> 8);
        b[1] = (byte) (s1 >>> 0);
        b[2] = (byte) (s2 >>> 8);
        b[3] = (byte) (s2 >>> 0);

        return bytesToInt(b, false);
    }

    /**
     * convert bytes into integer
     * @param bytes           bytes array
     * @param swapBytes       if need to swap
     * @return   integer
     */
    public static int bytesToInt(byte[] bytes, boolean swapBytes) {
        byte a = bytes[0];
        byte b = bytes[1];
        byte c = bytes[2];
        byte d = bytes[3];

        if (swapBytes) {
            return ((a & 0xff)) + ((b & 0xff) << 8) + ((c & 0xff) << 16) + ((d & 0xff) << 24);
        } else {
            return ((a & 0xff) << 24) + ((b & 0xff) << 16) + ((c & 0xff) << 8) + ((d & 0xff));
        }
    }

    /**
     * Concatenate two bytes to a 32-bit int value.  <b>a</b> is the high order
     * byte in the resulting int representation, unless swapBytes is true, in
     * which <b>b</b> is the high order byte.
     * @param a high order byte
     * @param b low order byte
     * @param swapBytes byte order swap flag
     * @return 32-bit integer
     */
    public static int bytesToInt(byte a, byte b, boolean swapBytes) {

        // again, high order bit is expressed left into 32-bit form
        if (swapBytes) {
            return (a & 0xff) + ((int) b << 8);
        } else {
            return ((int) a << 8) + (b & 0xff);
        }
    }

    /**
     * convert unsigned byte to short
     * @param b convert this unsigned byte
     * @return unsigned short
     */
    public short convertunsignedByte2Short(byte b) {
        return (short) ((b < 0)
                        ? (short) b + 256
                        : (short) b);
    }

    /**
     *  convert short to unsigned integer
     * @param b  convert this short
     * @return   unsigned integer
     */
    public int convertShort2unsignedInt(short b) {
        return (b < 0)
               ? (-1) * b + 32768
               : b;
    }

    /**
     * get jave date
     * @param julianDays
     * @param msecs
     * @return   java date
     */
    static public java.util.Date getDate(int julianDays, int msecs) {
        long total = ((long) (julianDays - 1)) * 24 * 3600 * 1000 + msecs;

        return new Date(total);
    }

    /**
     * Flush all data buffers to disk.
     * @throws IOException
     */
    public void flush() throws IOException {
        raf.flush();
    }

    /**
     *  Close the file.
     * @throws IOException
     */
    public void close() throws IOException {
        if (raf != null) {
            raf.close();
        }
    }

    // variable info for reading/writing
    static class Vinfo {
        long    hoff;        // header offset
        int     xt;
        int     yt;

        Vinfo(int xt, int yt, long hoff) {
            this.xt       = xt;
            this.yt       = yt;
            this.hoff     = hoff;
        }
    }
}