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

package ucar.nc2.grib.collection;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import thredds.featurecollection.FeatureCollectionConfig;
import ucar.nc2.grib.coord.Coordinate;
import ucar.nc2.grib.coord.CoordinateRuntime;
import ucar.nc2.grib.coord.CoordinateTime2D;
import ucar.nc2.grib.coord.CoordinateTimeAbstract;
import ucar.nc2.dataset.DatasetUrl;
import ucar.nc2.ft2.coverage.SubsetParams;
import ucar.nc2.grib.GdsHorizCoordSys;
import ucar.nc2.grib.GribIndexCache;
import ucar.nc2.grib.coord.TimeCoordIntvValue;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.util.CancelTask;
import ucar.nc2.util.Misc;
import ucar.nc2.util.cache.FileCacheable;
import ucar.nc2.util.cache.FileFactory;
import ucar.nc2.util.cache.SmartArrayInt;
import ucar.unidata.io.RandomAccessFile;

import javax.annotation.concurrent.Immutable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Formatter;
import java.util.List;

/**
 * An Immutable PartitionCollection
 *
 * @author caron
 * @since 11/10/2014
 */
public abstract class PartitionCollectionImmutable extends GribCollectionImmutable {
  private static final Logger logger = LoggerFactory.getLogger(PartitionCollectionImmutable.class);
  public static int countPC;   // debug

  static final ucar.nc2.util.cache.FileFactory partitionCollectionFactory = new FileFactory() {
    public FileCacheable open(DatasetUrl durl, int buffer_size, CancelTask cancelTask, Object iospMessage) throws IOException {
      try (RandomAccessFile raf = RandomAccessFile.acquire(durl.trueurl)) {
        Partition p = (Partition) iospMessage;
        return GribCdmIndex.openGribCollectionFromIndexFile(raf, p.getConfig(), p.getLogger()); // do we know its a partition ?

      } catch (Throwable t) {
        RandomAccessFile.eject(durl.trueurl);
        throw t;
      }
    }
  };

  ///////////////////////////////////////////////
  private final List<Partition> partitions;
  private final boolean isPartitionOfPartitions;
  private final int[] run2part;   // masterRuntime.length; which partition to use for masterRuntime i

  PartitionCollectionImmutable(PartitionCollectionMutable pc) {
    super(pc);

    List<PartitionCollectionMutable.Partition> pcParts = pc.partitions;
    List<Partition> work = new ArrayList<>(pcParts.size());
    for (PartitionCollectionMutable.Partition pcPart : pcParts) {
      work.add(new Partition(pcPart));
    }

    this.partitions = Collections.unmodifiableList(work);
    this.isPartitionOfPartitions = pc.isPartitionOfPartitions;
    this.run2part = pc.run2part;
  }

  // return open GC
  public GribCollectionImmutable getLatestGribCollection(List<String> paths) throws IOException {
    Partition last = partitions.get(partitions.size() - 1);
    paths.add(last.getName());

    GribCollectionImmutable gc = last.getGribCollection();
    if (gc instanceof PartitionCollectionImmutable) {
      try {
        PartitionCollectionImmutable pc = (PartitionCollectionImmutable) gc;
        return pc.getLatestGribCollection(paths);
      } finally {
        gc.close();  // make sure its closed even on exception
      }
    } else {
      return gc;
    }

  }

  protected VariableIndex makeVariableIndex(GroupGC group, GribCollectionMutable.VariableIndex mutableVar) {
    return new VariableIndexPartitioned(group, mutableVar);
  }

  public Iterable<Partition> getPartitions() {
    return partitions;
  }

  private Partition getPartition(int idx) {
    return partitions.get(idx);
  }

  @Nullable
  public Partition getPartitionByName(String name) {
    for (Partition p : partitions)
      if (p.name.equalsIgnoreCase(name)) return p;
    return null;
  }

  public boolean isPartitionOfPartitions() {
    return isPartitionOfPartitions;
  }

  public int getPartitionSize() {
    return partitions.size();
  }

  public List<Partition> getPartitionsSorted() {
    List<Partition> c = new ArrayList<>(partitions);
    Collections.sort(c);
    if (!this.config.getSortFilesAscending()) {
      Collections.reverse(c);
    }
    return c;
  }

  @Nullable
  private VariableIndexPartitioned getVariable2DByHash(GribHorizCoordSystem hcs, VariableIndex vi) {
    Dataset ds2d = getDatasetCanonical();
    if (ds2d == null) return null;
    for (GroupGC groupHcs : ds2d.getGroups())
      if (groupHcs.getGdsHash().equals(hcs.getGdsHash()))
        return (VariableIndexPartitioned) groupHcs.findVariableByHash(vi);
    return null;
  }

  @Override
  public void showIndex(Formatter f) {
    super.showIndex(f);
    f.format("%nPartition isPartitionOfPartitions = %s%n", isPartitionOfPartitions);
    for (Partition p : partitions) {
      f.format("  %s%n", p);
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // stuff for Iosp

  RandomAccessFile getRaf(int partno, int fileno) throws IOException {
    Partition part = getPartition(partno);
    try (GribCollectionImmutable gc = part.getGribCollection()) {
      return gc.getDataRaf(fileno);
    }
  }

  // debugging
  public String getFilename(int partno, int fileno) throws IOException {
    Partition part = getPartition(partno);
    try (GribCollectionImmutable gc = part.getGribCollection()) {
      return gc.getFilename(fileno);
    }
  }

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

  // wrapper around a GribCollection
  @Immutable
  public class Partition implements Comparable<Partition> {
    private final String name; // partDirectory;
    private final String filename;
    private final long lastModified, fileSize;
    private final CalendarDate partitionDate;

    // constructor from ncx
    public Partition(PartitionCollectionMutable.Partition pcPart) {
      this.name = pcPart.name;
      this.filename = pcPart.filename; // grib collection ncx
      this.lastModified = pcPart.lastModified;
      this.fileSize = pcPart.fileSize;
      //this.partDirectory = pcPart.directory;
      this.partitionDate = pcPart.partitionDate;
    }

    public String getName() {
      return name;
    }

    public String getFilename() {
      return filename;
    }

      /* public String getDirectory() {
        return partDirectory;
      } */

    public long getLastModified() {
      return lastModified;
    }

    public boolean isGrib1() {
      return isGrib1;         // in GribCollection
    }

    public FeatureCollectionConfig getConfig() {
      return config;   // in GribCollection
    }

    public org.slf4j.Logger getLogger() {
      return logger;          // in TimePartition
    }

    public String getIndexFilenameInCache() throws FileNotFoundException {
      File file = new File(directory, filename);
      File existingFile = GribIndexCache.getExistingFileOrCache(file.getPath());

      if (existingFile == null) {
        throw new FileNotFoundException("No index filename for partition= " + this.toString() + " looking for " + file.getPath());
      }

        /* if (existingFile == null) {
          if (Grib.debugIndexOnly) {  // we are running in debug mode where we only have the indices, not the data files
            // tricky: substitute the current root
            File orgParentDir = new File(directory);
            File currentFile = new File(PartitionCollectionImmutable.this.indexFilename);
            File currentParent = currentFile.getParentFile();
            File currentParentWithDir = new File(currentParent, orgParentDir.getName());
            File nestedIndex = isPartitionOfPartitions ? new File(currentParentWithDir, filename) : new File(currentParent, filename); // JMJ
            path = nestedIndex.getPath();

          } else {
            throw new FileNotFoundException("No index filename for partition= " + this.toString());
          } */

      return existingFile.getPath();
    }

    // acquire or construct GribCollection - caller must call gc.close() when done
    public GribCollectionImmutable getGribCollection() throws IOException {
      String path = getIndexFilenameInCache();
      return GribCdmIndex.acquireGribCollection(partitionCollectionFactory, path, path, -1, null, this);
    }

    @Override
    public int compareTo(@Nonnull Partition o) {
      return name.compareTo(o.name);
    }

    @Override
    public String toString() {
      return "Partition{ " + name + '\'' +
              //        ", directory='" + directory + '\'' +
              ", filename='" + filename + '\'' +
              ", partitionDate='" + partitionDate + '\'' +
              ", lastModified='" + CalendarDate.of(lastModified) + '\'' +
              ", fileSize='" + fileSize + '\'' +
              '}';
    }

  }

  @Immutable
  public class VariableIndexPartitioned extends GribCollectionImmutable.VariableIndex {
    final int nparts;
    final SmartArrayInt partnoSA;  // conceptually int[nparts] : index into PartitionCollectionImmutable.partitions[] -> Partition
    final SmartArrayInt groupnoSA; // once you have the partition, which group in that partition's dataset? Partition.Dataset.Group[] -> Group
    final SmartArrayInt varnoSA;   // once you have the group, which variable? Group[] -> Variable

    // partition only
    // final SmartArrayInt time2runtime; // oneD only: for each timeIndex, which runtime coordinate does it use? 1-based so 0 = missing;
    // index into the corresponding 2D variable's runtime coordinate

    VariableIndexPartitioned(GribCollectionImmutable.GroupGC g, GribCollectionMutable.VariableIndex other) {
      super(g, other);

      PartitionCollectionMutable.VariableIndexPartitioned pother = (PartitionCollectionMutable.VariableIndexPartitioned) other;
      this.nparts = pother.nparts;
      // this.time2runtime =  pother.time2runtime;

      this.partnoSA = pother.partnoSA;
      this.groupnoSA = pother.groupnoSA;
      this.varnoSA = pother.varnoSA;
    }

    public int getNparts() {
      return nparts;
    }

    public GribCollectionImmutable.Type getType() {
      return group.ds.gctype;
    }

    public void show(Formatter sb) {
      sb.format("VariableIndexPartitioned%n");
      sb.format(" partno=");
      this.partnoSA.show(sb);
      sb.format("%n groupno=");
      this.groupnoSA.show(sb);
      sb.format("%n varno=");
      this.varnoSA.show(sb);
      //sb.format("%n flags=");
      //for (PartitionForVariable2D partVar : partList)
      //  sb.format("%d,", partVar.flag);
      sb.format("%n");
      int count = 0;
      sb.format("     %7s %3s %3s %6s %3s%n", "N", "dups", "Miss", "density", "partition");
      // int totalN = 0, totalDups = 0, totalMiss = 0;
      for (int i = 0; i < nparts; i++) {
        int partWant = this.partnoSA.get(i);
        Partition part = partitions.get(partWant);
        sb.format("   %2d: %7d %s%n", count++, partWant, part.getFilename());
        //sb.format("   %2d: %7d %3d %3d   %6.2f   %d %s%n", count++, partVar.nrecords, partVar.ndups, partVar.missing, partVar.density, partVar.partno, part.getFilename());
        //totalN += partVar.nrecords;
        //totalDups += partVar.ndups;
        //totalMiss += partVar.missing;
      }
      //sb.format("total: %4d %3d %3d %n", totalN, totalDups, totalMiss);
      sb.format("%n");

      // sb.format(super.toStringComplete());
      // return sb.toString();
    }

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

    /**
     * find the data record for a request
     *
     * @param indexWanted the source index request, excluding x and y
     * @return DataRecord pointing to where the data is, or null if missing
     */
    @Nullable
    DataRecord getDataRecord(int[] indexWanted) throws IOException {

      if (Grib.debugRead)
        logger.debug("%nPartitionCollection.getDataRecord index wanted = (%s) on %s type=%s%n",
                Misc.showInts(indexWanted), indexFilename, group.ds.gctype);

      // find the runtime index
      int firstIndex = indexWanted[0];
      int masterIdx;
      if (group.ds.gctype == Type.TwoD) {
        // find the partition by matching run coordinate with master runtime
        CoordinateRuntime runtime = (CoordinateRuntime) getCoordinate(Coordinate.Type.runtime);
        if (runtime == null) {
          throw new IllegalStateException("Type.TwoD must have runtime coordinate");
        }
        Object val = runtime.getValue(firstIndex);
        masterIdx = masterRuntime.getIndex(val);
        if (Grib.debugRead)
          logger.debug("  TwoD firstIndex = %d val=%s masterIdx=%d %n", firstIndex, val, masterIdx);

      } else if (group.ds.gctype == Type.Best) {
        // find the partition from the "time2runtime" array in the time coordinate
        CoordinateTimeAbstract time = getCoordinateTime();
        if (time == null) {
          throw new IllegalStateException("Type.Best must have time coordinate");
        }
        masterIdx = time.getMasterRuntimeIndex(firstIndex) - 1;
        if (Grib.debugRead) logger.debug("  Best firstIndex = %d masterIdx=%d %n", firstIndex, masterIdx);

      } else if (group.ds.gctype == Type.MRUTP) {
        CoordinateTime2D time2D = (CoordinateTime2D) getCoordinateTime();
        if (time2D == null) {
          throw new IllegalStateException("Type.MRUTP must have time coordinate");
        }
        Object val = time2D.getRefDate(firstIndex);
        masterIdx = masterRuntime.getIndex(val);

        if (Grib.debugRead) logger.debug("  MRUTP firstIndex = %d masterIdx=%d %n", firstIndex, masterIdx);

      } else {
        throw new IllegalStateException("Unknown gctype= " + group.ds.gctype + " on " + indexFilename);
      }

      int partno = run2part[masterIdx];
      if (partno < 0) {
        return null; // LOOK is this possible?
      }

      // find the 2D vi in that partition
      GribCollectionImmutable.VariableIndex vindex2Dpart = getVindex2D(partno); // the 2D component variable in the partno partition
      if (vindex2Dpart == null) return null; // missing
      if (Grib.debugRead) logger.debug("  compVindex2D = %s%n", vindex2Dpart.toStringFrom());

      if (isPartitionOfPartitions) {
        VariableIndexPartitioned compVindex2Dp = (VariableIndexPartitioned) vindex2Dpart;
        return getDataRecordPofP(indexWanted, compVindex2Dp);
      }

      // translate to coordinates in vindex
      int[] sourceIndex;
      if (group.getType() == Type.Best)
        sourceIndex = translateIndexBest(indexWanted, vindex2Dpart);
      else
        sourceIndex = translateIndex2D(indexWanted, vindex2Dpart);

      if (sourceIndex == null) return null; // missing
      GribCollectionImmutable.Record record = vindex2Dpart.getRecordAt(sourceIndex);
      if (record == null) {
        return null;
      }

      if (Grib.debugRead) logger.debug("  result success: partno=%d fileno=%d %n", partno, record.fileno);
      return new DataRecord(PartitionCollectionImmutable.this, partno, vindex2Dpart.group.getGdsHorizCoordSys(), record);
    }

    /**
     * find DataRecord in a PofP
     *
     * @param indexWanted   index into this PoP
     * @param compVindex2Dp 2D variable from the desired partition; may be PofP or PofGC
     * @return desired record to be read, from the GC, or null if missing
     */
    @Nullable
    private DataRecord getDataRecordPofP(int[] indexWanted, VariableIndexPartitioned compVindex2Dp) throws IOException {
      if (group.getType() == Type.Best) {
        int[] indexWantedP = translateIndexBest(indexWanted, compVindex2Dp);
        if (Grib.debugRead) logger.debug("  (Best) getDataRecordPofP= %s %n", Misc.showInts(indexWantedP));
        if (indexWantedP == null) return null;
        return compVindex2Dp.getDataRecord(indexWantedP);

      } else {
        // corresponding index into compVindex2Dp
        int[] indexWantedP = translateIndex2D(indexWanted, compVindex2Dp);
        if (Grib.debugRead) logger.debug("  (2D) getDataRecordPofP= %s %n", Misc.showInts(indexWantedP));
        if (indexWantedP == null) return null;
        return compVindex2Dp.getDataRecord(indexWantedP);

      } /* else if (group.getType() == Type.Best) {
        int[] indexWantedP = translateIndexBest(indexWanted, compVindex2Dp);
        if (Grib.debugRead) logger.debug("  (Best) getDataRecordPofP= %s %n", Misc.showInts(indexWantedP));
        if (indexWantedP == null) return null;
        return compVindex2Dp.getDataRecord(indexWantedP);

      } /* else if (group.getType() == Type.MRUTP) {
        int[] indexWantedP = translateIndex1D(masterIdx, indexWanted, compVindex2Dp);
        if (Grib.debugRead) logger.debug("  (1D) getDataRecordPofP= %s %n", Misc.showInts(indexWantedP));
        if (indexWantedP == null) return null;
        return compVindex2Dp.getDataRecord(indexWantedP);
      } */

      // throw new IllegalStateException("Not handling Type "+group.getType());
    }

    /**
     * Get VariableIndex (2D) for this partition
     *
     * @param partno master partition number
     * @return VariableIndex or null if not exists
     */
    @Nullable
    private GribCollectionImmutable.VariableIndex getVindex2D(int partno) throws IOException {
      // at this point, we need to instantiate the Partition and the vindex.records

      // the 2D vip for this variable
      VariableIndexPartitioned vip = isPartitionOfPartitions ? getVariable2DByHash(group.horizCoordSys, this) : this;

      if (vip == null)
        throw new IllegalStateException();

      int partWant = vip.partnoSA.findIdx(partno); // which partition ? index into PartitionCollectionImmutable.partitions[]. variable doesnt have to exist in all partitions
      if (partWant < 0 || partWant >= vip.nparts) {
        if (Grib.debugRead) logger.debug("  cant find partition=%d in vip=%s%n", partno, vip);
        return null;
      }
      // LOOK was partVar = new PartitionForVariable2D(partnoSA.get(idx), groupnoSA.get(idx), varnoSA.get(idx));
      //        GroupGC g = ds.groups.get(partVar.groupno);
      //        GribCollection.VariableIndex vindex = g.variList.get(partVar.varno);
      // now

      Partition p = getPartition(partno);
      try (GribCollectionImmutable gc = p.getGribCollection()) { // ensure that its read in try-with
        GribCollectionImmutable.Dataset ds = gc.getDatasetCanonical(); // always references the twoD or GC dataset
        // the group and variable index may vary across partitions
        GribCollectionImmutable.GroupGC g = ds.groups.get(vip.groupnoSA.get(partWant));         // LOOK partWant vs partno ??
        GribCollectionImmutable.VariableIndex vindex = g.variList.get(vip.varnoSA.get(partWant));
        vindex.readRecords();
        return vindex;
      }  // LOOK opening the file here, and then again to read the data. partition cache helps i guess but we could do better i think.
    }

    /*
     * MRUTP
     * translate index in VariableIndexPartitioned to corresponding index in one of its component VariableIndex (which will be 2D)
     * by matching coordinate values.
     *
     * @param wholeIndex   index in VariableIndexPartitioned, runtime has been added
     * @param vindex2Dpart component 2D VariableIndex
     * @return corresponding index in compVindex2D, or null if missing
     *
    private int[] translateIndex1D(int[] wholeIndex, GribCollectionImmutable.VariableIndex vindex2Dpart) {
      int[] result = new int[wholeIndex.length];

      // figure out the runtime index in the partition
      Coordinate runtime = vindex2Dpart.getCoordinate(0);
      int runtimeIdxPart = matchCoordinate(masterRuntime, masterIdx, runtime);
      if (runtimeIdxPart < 0)
        return null;         // LOOK is this possible ?? should throw exception??
      result[0] = runtimeIdxPart;

      // figure out the other indexes in the partition
      // assumes gc coordinates in same order as iosp
      int countDim = 0;
      while (countDim < wholeIndex.length) {
        Coordinate wholeCoord = getCoordinate(countDim + 1);
        int idx = wholeIndex[countDim];

        Coordinate coordPart = vindex2Dpart.getCoordinate(countDim + 1); // wholeIndex(time, vert) -> vip(runtime, time, vert)
        int resultIdx;
        if (coordPart instanceof CoordinateTime2D) {
          CoordinateTime2D coordPart2D = (CoordinateTime2D) coordPart; // partition coordinate
          CoordinateTime2D coordWhole2D = (CoordinateTime2D) wholeCoord; // whole coordinate
          CoordinateTime2D.Time2D wholeVal = coordWhole2D.getOrgValue(masterIdx, idx);
          if (wholeVal == null)  // is this possible?
            return null;

          resultIdx = coordPart2D.matchTimeCoordinate(runtimeIdxPart, wholeVal);
          // if (resultIdx < 0) resultIdx = compCoord2D.matchTimeCoordinate(runtimeIdxPart, wholeVal, wholeCoord1Dtime.getRefDate()); // debug

        } else {
          resultIdx = matchCoordinate(wholeCoord, idx, coordPart);
          // if (resultIdx < 0) resultIdx = matchCoordinate(wholeCoord1D, idx, compCoord); // debug
        }
        if (resultIdx < 0) {
          // logger.info("Couldnt match coordinates ({}) for variable {}", Misc.showInts(wholeIndex), compVindex2D.toStringFrom());
          return null;
        }
        result[countDim + 1] = resultIdx;
        countDim++;
      }

      return result;
    } */

    /**
     * Best
     * translate index in VariableIndexPartitioned to corresponding index in one of its component VariableIndex (which will be 2D)
     * by matching coordinate values.
     *
     * @param wholeIndex   index in VariableIndexPartitioned
     * @param compVindex2D component 2D VariableIndex
     * @return corresponding index in compVindex2D, or null if missing
     */
    @Nullable
    private int[] translateIndexBest(int[] wholeIndex, GribCollectionImmutable.VariableIndex compVindex2D) {
      int[] result = new int[wholeIndex.length + 1];

      // figure out the runtime
      int timeIdx = wholeIndex[0];
      CoordinateTimeAbstract time = getCoordinateTime();
      assert time != null;
      int masterIdx = time.getMasterRuntimeIndex(timeIdx) - 1;
      int runtimeIdxPart = matchCoordinate(masterRuntime, masterIdx, compVindex2D.getCoordinate(0));
      if (runtimeIdxPart < 0)
        return null;         // LOOK is this possible ??
      result[0] = runtimeIdxPart;

      // figure out the time and any other dimensions
      int countDim = 0;
      while (countDim < wholeIndex.length) {
        Coordinate wholeCoord1D = getCoordinate(countDim);
        int idx = wholeIndex[countDim];

        Coordinate compCoord = compVindex2D.getCoordinate(countDim + 1);
        int resultIdx;
        if (compCoord.getType() == Coordinate.Type.time2D) {
          CoordinateTime2D compCoord2D = (CoordinateTime2D) compCoord; // of the component
          CoordinateTimeAbstract wholeCoord1Dtime = (CoordinateTimeAbstract) wholeCoord1D;// CoordinateTime or CoordinateTimeIntv
          Object wholeVal1D = wholeCoord1D.getValue(idx);
          if (wholeVal1D == null)  // is this possible?
            return null;

          CoordinateTime2D.Time2D wholeVal2D = compCoord2D.isTimeInterval() ?
                  new CoordinateTime2D.Time2D(wholeCoord1Dtime.getRefDate(), null, (TimeCoordIntvValue) wholeVal1D) :
                  new CoordinateTime2D.Time2D(wholeCoord1Dtime.getRefDate(), (Integer) wholeVal1D, null);

          resultIdx = compCoord2D.matchTimeCoordinate(runtimeIdxPart, wholeVal2D);
          // if (resultIdx < 0) resultIdx = compCoord2D.matchTimeCoordinate(runtimeIdxPart, wholeVal, wholeCoord1Dtime.getRefDate()); // debug

        } else {
          resultIdx = matchCoordinate(wholeCoord1D, idx, compCoord);
          // if (resultIdx < 0) resultIdx = matchCoordinate(wholeCoord1D, idx, compCoord); // debug
        }
        if (resultIdx < 0) {
          // logger.info("Couldnt match coordinates ({}) for variable {}", Misc.showInts(wholeIndex), compVindex2D.toStringFrom());
          return null;
        }
        result[countDim + 1] = resultIdx;
        countDim++;
      }

      return result;
    }

    /**
     * TwoD
     * Given the index in the whole (wholeIndex), translate to index in component (compVindex2D) by matching the coordinate values
     *
     * @param wholeIndex   index in the whole
     * @param compVindex2D want index in here
     * @return index into  compVindex2D, or null if missing
     */
    @Nullable
    private int[] translateIndex2D(int[] wholeIndex, GribCollectionImmutable.VariableIndex compVindex2D) {
      int[] result = new int[wholeIndex.length];
      int countDim = 0;

      // special case for 2D time
      CoordinateTime2D compTime2D = (CoordinateTime2D) compVindex2D.getCoordinate(Coordinate.Type.time2D);
      if (compTime2D != null) {
        CoordinateTime2D time2D = (CoordinateTime2D) getCoordinate(Coordinate.Type.time2D);
        if (time2D == null) throw new IllegalStateException("CoordinateTime2D has no time2D");
        CoordinateTime2D.Time2D want = time2D.getOrgValue(wholeIndex[0], wholeIndex[1]);
        if (Grib.debugRead)
          logger.debug("  translateIndex2D[runIdx=%d, timeIdx=%d] in componentVar coords = (%s,%s) %n",
                  wholeIndex[0], wholeIndex[1], (want == null) ? "null" : want.getRefDate(), want);
        if (want == null) {
          // time2D.getOrgValue(wholeIndex[0], wholeIndex[1], Grib.debugRead); // debug
          return null;
        }
        if (!compTime2D.getIndex(want, result)) {// sets the first 2 indices - run and time
          // compTime2D.getIndex(want, result); // debug
          return null; // missing data
        }
        countDim = 2;
      }

      // the remaining dimensions, if any
      while (countDim < wholeIndex.length) {
        int idx = wholeIndex[countDim];
        int resultIdx = matchCoordinate(getCoordinate(countDim), idx, compVindex2D.getCoordinate(countDim));
        if (Grib.debugRead) logger.debug("  translateIndex2D[idx=%d] resultIdx= %d %n", idx, resultIdx);
        if (resultIdx < 0) {   // partition variable doesnt have a coordinate value that is in the "whole" variable
          return null;
        }
        result[countDim] = resultIdx;
        countDim++;
      }

      return result;
    }

    private int matchCoordinate(Coordinate whole, int wholeIdx, Coordinate part) {
      Object val = whole.getValue(wholeIdx);
      if (val == null)
        return -1;
      return part.getIndex(val);
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    // experimental coord based
    @Nullable
    DataRecord getDataRecord(SubsetParams coords) throws IOException {

      // identify the master index for this runtime
      CalendarDate runtime = coords.getRunTime();
      int masterIdx = masterRuntime.getIndex(runtime.getMillis());
      // LOOK ok to use Best like this (see other getDataRecord) ?

      if (masterIdx < 0) { // means that the runtie is not in the masterRuntime list
        throw new RuntimeException("masterRuntime does not contain runtime " + runtime);
      }

      // each runtime is mapped to a partition
      int partno = run2part[masterIdx];
      if (partno < 0)
        return null; // LOOK is this possible?

      // find the 2D vi in that partition
      GribCollectionImmutable.VariableIndex compVindex2D = getVindex2D(partno); // the 2D component variable in the partno partition
      if (compVindex2D == null) return null; // missing
      if (Grib.debugRead) logger.debug("  compVindex2D = %s%n", compVindex2D.toStringFrom());

      if (isPartitionOfPartitions) {
        VariableIndexPartitioned compVindex2Dp = (VariableIndexPartitioned) compVindex2D;
        return compVindex2Dp.getDataRecord(coords);
      }

      // otherwise its a GribCollection
      GribCollectionImmutable.Record record = compVindex2D.getRecordAt(coords);
      if (record == null)
        return null;

      if (Grib.debugRead) logger.debug("  result success: partno=%d fileno=%d %n", partno, record.fileno);
      DataRecord dr = new DataRecord(PartitionCollectionImmutable.this, partno, compVindex2D.group.getGdsHorizCoordSys(), record);
      if (GribDataReader.validator != null) dr.validation = coords;
      return dr;
    }

  }

  @Immutable
  class DataRecord extends GribDataReader.DataRecord {
    final PartitionCollectionImmutable usePartition;
    final int partno; // partition index in usePartition

    DataRecord(PartitionCollectionImmutable usePartition, int partno, GdsHorizCoordSys hcs, GribCollectionImmutable.Record record) {
      super(-1, record, hcs);
      this.usePartition = usePartition;
      this.partno = partno;
    }

    @Override
    public int compareTo(@Nonnull GribDataReader.DataRecord o) {
      DataRecord op = (DataRecord) o;
      int rp = usePartition.getName().compareTo(op.usePartition.getName());
      if (rp != 0) return rp;
      int r = Misc.compare(partno, op.partno);
      if (r != 0) return r;
      r = Misc.compare(record.fileno, o.record.fileno);
      if (r != 0) return r;
      return Misc.compare(record.pos, o.record.pos);
    }

    boolean usesSameFile(DataRecord o) {
      if (o == null) return false;
      int rp = usePartition.getName().compareTo(o.usePartition.getName());
      if (rp != 0) return false;
      int r = Misc.compare(partno, o.partno);
      if (r != 0) return false;
      r = Misc.compare(record.fileno, o.record.fileno);
      return r == 0;
    }

    //debugging
    public void show() throws IOException {
      String dataFilename = usePartition.getFilename(partno, record.fileno);
      System.out.printf(" **DataReader partno=%d fileno=%d filename=%s startPos=%d%n", partno, record.fileno, dataFilename, record.pos);
    }
  }
}


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

package ucar.nc2.iosp.uamiv;

import ucar.ma2.*;

import ucar.nc2.*;
import ucar.nc2.constants.CDM;
import ucar.nc2.iosp.AbstractIOServiceProvider;
import ucar.nc2.util.CancelTask;

import ucar.unidata.io.RandomAccessFile;

import java.io.*;
import java.io.File;
import java.util.HashSet;
import java.util.Arrays;

/**
 * Class for reading CAMx flavored uamiv files.
 * CAMx UAM-IV formatted files.
 * uses "IOAP Conventions", handled by M3IO CoordSysBuilder
 *
 * @author Barron Henderson barronh@gmail.com
 * @see "http://www.camx.com/"
 */
public class UAMIVServiceProvider extends AbstractIOServiceProvider {
  static private org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(UAMIVServiceProvider.class);

  static private final String AVERAGE =   "A   V   E   R   A   G   E               ";
  static private final String EMISSIONS = "E   M   I   S   S   I   O   N   S       ";
  static private final String AIRQUALITY ="A   I   R   Q   U   A   L   I   T   Y   ";
  static private final String INSTANT =   "I   N   S   T   A   N   T               ";

  static private final String HEIGHT = "HEIGHT";
  static private final String PBL = "PBL";

  static private final String TEMP = "TEMP";

  static private final String PRESS = "PRESS";

  static private final String WINDX = "WINDX";
  static private final String WINDY = "WINDY";
  static private final String VERTDIFF = "Kv";
  static private final String SPEED = "SPEED";

  static private final String CLDOD = "CLD OPDEP";

  static private final String CLDWATER = "CLD WATER";
  static private final String PRECIP = "PCP WATER";
  static private final String RAIN = "RAIN";

  private String[] species_names;
  private long data_start;
  private int n2dvals;
  private int n3dvals;
  private int spc_3D_block;
  private int data_block;

  /**
   * Check if this is a valid file for this IOServiceProvider.
   *
   * @param raf RandomAccessFile
   * @return true if valid.
   */
  public boolean isValidFile(RandomAccessFile raf) throws IOException {
    try {
      raf.order(RandomAccessFile.BIG_ENDIAN);
      raf.seek(0);
      raf.skipBytes(4);
      String test = raf.readString(40);
      return test.equals(EMISSIONS) || test.equals(AVERAGE) || test.equals(AIRQUALITY) || test.equals(INSTANT);
    } catch (IOException ioe) {
      return false;
    }
  }

  public String getFileTypeId() {
    return "UAMIV";
  }

  public String getFileTypeDescription() {
    return "CAMx UAM-IV formatted files";
  }

  /**
   * Open existing file, and populate ncfile with it.
   *
   * @param raf        the file to work on, it has already passed the isValidFile() test.
   * @param ncfile     add objects to this NetcdfFile
   * @param cancelTask used to monito user cancellation; may be null.
   * @throws IOException
   */
  public void open(RandomAccessFile raf, NetcdfFile ncfile, CancelTask cancelTask) throws IOException {
    /*
     * open initializes the file meta data and creates all variables.
     * The meta-data and variable information is gathered from the UAM-IV
     * header.  The header format is detailed in the CAMx User's
     * guide and copied here.
     *
     * Header:
     * name,note,ione,nspec,ibdate,btime,iedate,etime
     * rdum,rdum,iutm,xorg,yorg,delx,dely,nx,ny,nz,idum,idum,rdum,rdum,rdum
     * ione,ione,nx,ny
     * (mspec(l),l=1,nspec)
     *
     * name - Text string (character*4(10) array)
     * note - Text string containing file description (character*4(60) array)
     * ione - Dummy variable = 1
     * nspec - Number of species on file
     * ibdate - Beginning date (YYJJJ)
     * btime - Beginning hour (HHMM)
     * iedate - Ending date (YYJJJ)
     * etime - Ending hour (HHMM)
     * rdum - Dummy real variable
     * iutm - UTM zone (ignored for other projections)
     * xorg - Grid x-origin at southwest corner of domain (m or degrees longitude)
     * yorg - Grid y-origin at southwest corner of domain (m or degrees latitude)
     * delx - Cell size in x-direction (m or degrees longitude)
     * dely - Cell size in y-direction (m or degrees longitude)
     * nx - Number of grid columns
     * ny - Number of grid rows
     * nz - Number of layers
     * idum - Dummy integer variable
     * mspec - Species names for nspec species (character*4(10,nspec) array)
     *
     *
     *   time step is HHMMSS
     *
     *  the projection is:
     *   LCC // >  :GDTYP = 2; // int
     *   First True Latitude (Alpha):  	30N // >  :P_ALP = 30.0; // double
     *   Second True Latitude (Beta): 	60N // >  :P_BET = 60.0; // double
     *   Central Longitude (Gamma): 	100W //>  :XCENT = -100.0; // double
     *   Projection Origin: 	(100W, 40N) //>  :YCENT = 40.0; // double
     *
     */
    // Internalize raf and ncfile
    super.open(raf, ncfile, cancelTask);

    // set raf to big endian and start at the beginning
    raf.order(RandomAccessFile.BIG_ENDIAN);
    raf.seek(0);

    // Read first line of UAM-IV header
    raf.skipBytes(4); // Skip record pad
    String name = raf.readString(40); // read 40 name
    String note = raf.readString(240);
    int itzone = raf.readInt(); // Read the time zone
    int nspec = raf.readInt(); // Read number of species
    int bdate = raf.readInt(); // get file start date
    float btime = raf.readFloat(); // get file start time
    int edate = raf.readInt(); // get file end date
    float etime = raf.readFloat(); // get file end time
    int btimei = (int) btime; // convert btime to an integer

    // CAMx times are sometimes provided as HH or HHMM.
    // IOAPI times are always provided as HHMMSS.
    // CAMx times less than 100 are HH and should be
    // multipled by 100 to get HHMM.  CAMx times less
    // 10000 are HHMM and should be multipled by 100
    // to get HHMMSS.
    if (btimei < 100) btimei = btimei * 100;
    if (btimei < 10000) btimei = btimei * 100;

    /*
    * Dates are YYJJJ and are heuristically converted
    * to YYYYJJJ based on the following assumption:
    * YY < 70 are 2000
    * YY >= 70 are 1900
    *
    */
    if (bdate < 70000) {
      edate = edate + 2000000;
      bdate = bdate + 2000000;
    } else {
      edate = edate + 1900000;
      bdate = bdate + 1900000;
    }

    raf.skipBytes(4); //Skip record pad

    // Read second line of UAM-IV header
    raf.skipBytes(4); //Skip record pad
    float plon = raf.readFloat(); // get polar longitude
    float plat = raf.readFloat(); // get polar latitude
    int iutm = raf.readInt(); // get utm
    float xorg = raf.readFloat(); // get x origin in meters
    float yorg = raf.readFloat(); // get y origin in meters
    float delx = raf.readFloat(); // get x cell size in meters
    float dely = raf.readFloat(); // get y cell size in meters
    int nx = raf.readInt(); // get number of columns
    int ny = raf.readInt(); // get number of rows
    int nz = raf.readInt(); // get number of layers
    // get projection number
    //    (0: lat-lon;
    //     1: Universal Transverse Mercator;
    //     2: Lambert Conic Conformal;
    //     3: Polar stereographic)
    // These translate to IOAPI GDTYP3D values 1, 5, 2, and 6 respectively
    int iproj = raf.readInt();
    int istag = raf.readInt(); // Read stagger indicator
    float tlat1 = raf.readFloat(); // Read true latitude 1
    float tlat2 = raf.readFloat(); // Read true latitude 2
    raf.skipBytes(4); //Skip 1 dummies
    raf.skipBytes(4); //Skip record pad

    // Read third line of UAM-IV header
    raf.skipBytes(4); //Skip record pad
    raf.skipBytes(8); //Skip 2 dummies
    int nx2 = raf.readInt(); // duplicate number of columns
    int ny2 = raf.readInt(); // duplicate number of rows
    raf.skipBytes(8); //Skip 2 dummies
    nz = Math.max(nz, 1); // number of layers; Emissions files occasionally report 0 layers
    /*
     * 1) Read each species name
     * 2) remove white space from the name
     * 3) store the names
     * 4) internalize them
     */
    int count = 0;
    String[] spc_names = new String[nspec];
    while (count < nspec) {
      String spc = raf.readString(40); // 1) read species name
      spc_names[count++] = spc.replace(" ", ""); // 2&3) store name without whitespace
    }
    this.species_names = spc_names; // 4) internalize names
    raf.skipBytes(4); // Skip record pad

    // Note this position; it is the start of the data block
    this.data_start = raf.getFilePointer();

    // Note the number of float equivalents (4 byte chunks) in data block
    int data_length_float_equivalents = ((int) raf.length() - (int) data_start) / 4;

    // Store 2D value size
    this.n2dvals = nx * ny;

    // Store 3D value size
    this.n3dvals = nx * ny * nz;

    // Store 2D binary data block size: include values (nx*ny),
    // species name (10), a dummy (1) and 2 record pads
    int spc_2D_block = nx * ny + 10 + 2 + 1;

    // Store 3D binary data block size
    this.spc_3D_block = spc_2D_block * nz;

    // Store whole data block size; includes date (6)
    this.data_block = this.spc_3D_block * nspec + 6;

    // Store the number of times
    int ntimes = data_length_float_equivalents / this.data_block;

    // Add dimensions based on header values
    ncfile.addDimension(null, new Dimension("TSTEP", ntimes, true));
    ncfile.addDimension(null, new Dimension("LAY", nz, true));
    ncfile.addDimension(null, new Dimension("ROW", ny, true));
    ncfile.addDimension(null, new Dimension("COL", nx, true));

    // Force sync of dimensions
    ncfile.finish();
    count = 0;

    /*
    * For each species, create a variable with long_name,
    * and var_desc, and units.  long_name and var_desc are
    * simply the species name.  units is heuristically
    * determined from the name
    */
    HashSet<String> AeroSpcs = new HashSet<>(Arrays.asList( "PSO4", "PNO3", "PNH4", "PH2O", "SOPA", "SOPB",  "NA", "PCL", "POA", "PEC", "FPRM", "FCRS", "CPRM", "CCRS"));
    HashSet<String> LULC = new HashSet<>(Arrays.asList("WATER", "ICE", "LAKE", "ENEEDL", "EBROAD", "DNEEDL", "DBROAD", "TBROAD", "DDECID", "ESHRUB", "DSHRUB", "TSHRUB", "SGRASS", "LGRASS", "CROPS", "RICE", "SUGAR", "MAIZE", "COTTON", "ICROPS", "URBAN", "TUNDRA", "SWAMP", "DESERT", "MWOOD", "TFOREST"));

    while (count < nspec) {
      String spc = spc_names[count++];
      Variable temp = ncfile.addVariable(null, spc, DataType.FLOAT, "TSTEP LAY ROW COL");
      if (spc.equals(WINDX) || spc.equals(WINDY) ||
              spc.equals(SPEED)) {
        temp.addAttribute(new Attribute(CDM.UNITS, "m/s"));
      } else if (spc.equals(VERTDIFF)) {
        temp.addAttribute(new Attribute(CDM.UNITS, "m**2/s"));
      } else if (spc.equals(TEMP)) {
        temp.addAttribute(new Attribute(CDM.UNITS, "K"));
      } else if (spc.equals(PRESS)) {
        temp.addAttribute(new Attribute(CDM.UNITS, "hPa"));
      } else if (spc.equals(HEIGHT) || spc.equals(PBL)) {
        temp.addAttribute(new Attribute(CDM.UNITS, "m"));
      } else if (spc.equals(CLDWATER) || spc.equals(PRECIP) || spc.equals(RAIN)) {
        temp.addAttribute(new Attribute(CDM.UNITS, "g/m**3"));
      } else if (spc.equals(CLDOD) || spc.equals("CLOUDOD")) {
        temp.addAttribute(new Attribute(CDM.UNITS, "none"));
      } else if (spc.equals("SNOWCOVER")) {
        temp.addAttribute(new Attribute(CDM.UNITS, "yes/no"));
      } else if (spc.startsWith("SOA") || AeroSpcs.contains(spc)) {
        if (name.equals(EMISSIONS)) {
          temp.addAttribute(new Attribute(CDM.UNITS, "g/time"));
        } else {
          temp.addAttribute(new Attribute(CDM.UNITS, "ug/m**3"));
        }
      } else if (LULC.contains(spc)) {
          temp.addAttribute(new Attribute(CDM.UNITS, "fraction"));
      } else if (spc.lastIndexOf("_") > -1) {
        String tmpunit = spc.substring(spc.lastIndexOf("_") + 1);
        tmpunit = tmpunit.trim();
        switch (tmpunit) {
          case "M2pS":
            tmpunit = "m**2/s";
            break;
          case "MpS":
            tmpunit = "m/s";
            break;
          case "PPM":
            tmpunit = "ppm";
            break;
          case "MB":
            tmpunit = "millibar";
            break;
          case "GpM3":
            tmpunit = "g/m**3";
            break;
          case "M":
            tmpunit = "m";
            break;
        }
        temp.addAttribute(new Attribute(CDM.UNITS, tmpunit));
      } else {
        if (name.equals(EMISSIONS)) {
          temp.addAttribute(new Attribute(CDM.UNITS, "mol/time"));
        } else {
          temp.addAttribute(new Attribute(CDM.UNITS, "ppm"));
        }
      }
      temp.addAttribute(new Attribute(CDM.LONG_NAME, spc));
      temp.addAttribute(new Attribute("var_desc", spc));
    }

    /*
    * Create 1...n array of "sigma" values
    */
    double[] sigma = new double[nz + 1];
    count = 0;
    while (count < nz + 1) {
      sigma[count++] = count;
    }
    int[] size = new int[1];
    size[0] = nz + 1;
    Array sigma_arr = Array.factory(DataType.DOUBLE, size, sigma);

    /*
    * Add meta-data according to the IOAPI conventions
    * http://www.baronams.com/products/ioapi
    */
    ncfile.addAttribute(null, new Attribute("VGLVLS", sigma_arr));
    ncfile.addAttribute(null, new Attribute("SDATE", bdate));
    ncfile.addAttribute(null, new Attribute("STIME", btimei));
    ncfile.addAttribute(null, new Attribute("TSTEP", 10000));
    ncfile.addAttribute(null, new Attribute("NSTEPS", ntimes));
    ncfile.addAttribute(null, new Attribute("NLAYS", nz));
    ncfile.addAttribute(null, new Attribute("NROWS", ny));
    ncfile.addAttribute(null, new Attribute("NCOLS", nx));
    ncfile.addAttribute(null, new Attribute("XORIG", (double) xorg));
    ncfile.addAttribute(null, new Attribute("YORIG", (double) yorg));
    ncfile.addAttribute(null, new Attribute("XCELL", (double) delx));
    ncfile.addAttribute(null, new Attribute("YCELL", (double) dely));

    /*
     * IOAPI Projection parameters are provided by a colocated camxproj.txt file;
     *
     * to do:
     * 1) needs earth radius
     * 2) needs better error checking
    */
    Integer gdtyp = 2;
    Double p_alp = 20.;
    Double p_bet = 60.;
    Double p_gam = 0.;
    Double xcent = -95.;
    Double ycent = 25.;
    if (!((iproj == 0) && (tlat1 == 0) && (tlat2 == 0) && (plon == 0) && (plat == 0))) {
      xcent = (double) plon;
      ycent = (double) plat;
      if (iproj == 0) {
        // Lat-Lon (iproj=0) has no additional information
        gdtyp = 1;
      } else if (iproj == 1){
        // UTM uses only iutm
        gdtyp = 5;
        p_alp = (double) iutm;
      } else if (iproj == 2){
        gdtyp = 2;
        p_alp = (double) tlat1;
        p_bet = (double) tlat2;
        p_gam = (double) plon;
      } else if (iproj == 3){
        gdtyp = 6;
        if (plat == 90){
          p_alp = 1.;
        } else if (plat == -90) {
          p_alp = -1.;
        }
        p_bet = (double) tlat1;
        p_gam = (double) plon;
      } else {
        gdtyp = 2;
        p_alp = 20.;
        p_bet = 60.;
        p_gam = 0.;
        xcent = -95.;
        ycent = 25.;
      }
    }

    String thisLine;
    String projpath = raf.getLocation();
    Boolean lgdtyp = false;
    Boolean lp_alp = false;
    Boolean lp_bet = false;
    Boolean lp_gam = false;
    Boolean lxcent = false;
    Boolean lycent = false;
    int lastIndex = projpath.lastIndexOf(File.separator);
    if (lastIndex <= 0)
      lastIndex = projpath.lastIndexOf('/');
    if (lastIndex > 0)
      projpath = projpath.substring(0, lastIndex);
    projpath = projpath + File.separator + "camxproj.txt";
    File paramFile = new File(projpath);

    if (paramFile.exists()) {
      try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(paramFile), CDM.UTF8))) {
        while ((thisLine = br.readLine()) != null) {
          if (thisLine.length() == 0) continue;
          if (thisLine.charAt(0) == '#') continue;
          String[] key_value = thisLine.split("=");
          switch (key_value[0]) {
            case "GDTYP":
              gdtyp = Integer.parseInt(key_value[1]);
              lgdtyp = true;
              break;
            case "P_ALP":
              p_alp = Double.parseDouble(key_value[1]);
              lp_alp = true;
              break;
            case "P_BET":
              p_bet = Double.parseDouble(key_value[1]);
              lp_bet = true;
              break;
            case "P_GAM":
              p_gam = Double.parseDouble(key_value[1]);
              lp_gam = true;
              break;
            case "YCENT":
              ycent = Double.parseDouble(key_value[1]);
              lycent = true;
              break;
            case "XCENT":
              xcent = Double.parseDouble(key_value[1]);
              lxcent = true;
              break;
          }
        }
      }
      if (!lgdtyp) log.warn("GDTYP not found; using " + gdtyp.toString());
      if (!lp_alp) log.warn("P_ALP not found; using " + p_alp.toString());
      if (!lp_bet) log.warn("P_BET not found; using " + p_bet.toString());
      if (!lp_gam) log.warn("P_GAM not found; using " + p_gam.toString());
      if (!lxcent) log.warn("XCENT not found; using " + xcent.toString());
      if (!lycent) log.warn("YCENT not found; using " + ycent.toString());

    } else {
      if (log.isDebugEnabled()) log.debug("UAMIVServiceProvider: adding projection file");
      try (FileOutputStream out = new FileOutputStream(paramFile)) {
        OutputStreamWriter fout = new OutputStreamWriter(out, CDM.utf8Charset);
        BufferedWriter bw = new BufferedWriter(fout);

        bw.write("# Projection parameters are based on IOAPI.  For details, see www.baronams.com/products/ioapi/GRIDS.html");
        bw.newLine();
        bw.write("GDTYP=");
        bw.write(gdtyp.toString());
        bw.newLine();
        bw.write("P_ALP=");
        bw.write(p_alp.toString());
        bw.newLine();
        bw.write("P_BET=");
        bw.write(p_bet.toString());
        bw.newLine();
        bw.write("P_GAM=");
        bw.write(p_gam.toString());
        bw.newLine();
        bw.write("XCENT=");
        bw.write(xcent.toString());
        bw.newLine();
        bw.write("YCENT=");
        bw.write(ycent.toString());
        bw.newLine();
        bw.flush();
        bw.close();
      }
    }

    ncfile.addAttribute(null, new Attribute("GDTYP", gdtyp));
    ncfile.addAttribute(null, new Attribute("P_ALP", p_alp));
    ncfile.addAttribute(null, new Attribute("P_BET", p_bet));
    ncfile.addAttribute(null, new Attribute("P_GAM", p_gam));
    ncfile.addAttribute(null, new Attribute("XCENT", xcent));
    ncfile.addAttribute(null, new Attribute("YCENT", ycent));
  }

  /**
   * Read data from a top level Variable and return a memory resident Array.
   * This Array has the same element type as the Variable, and the requested shape.
   *
   * @param v2          a top-level Variable
   * @param wantSection List of type Range specifying the section of data to read.
   *                    There must be a Range for each Dimension in the variable, in order.
   *                    Note: no nulls.
   * @return the requested data in a memory-resident Array
   * @throws java.io.IOException
   * @throws ucar.ma2.InvalidRangeException
   * @see ucar.ma2.Range
   */
  public ucar.ma2.Array readData(Variable v2, Section wantSection) throws IOException, InvalidRangeException {
    /*
     * readData seeks and reads the data for each variable.  The variable
     * data format is detailed in the CAMx User's guide and summarized here.
     *
     * For each time:
     *   ibdate,btime,iedate,etime
     *   Loop from 1 to nspec species:
     *     ione,mspec(l),((val(i,j,l),i=1,nx),j=1,ny)
     *
     *
     * ione - Dummy variable = 1
     * nspec - Number of species on file
     * ibdate - Beginning date (YYJJJ)
     * btime - Beginning hour (HHMM)
     * iedate - Ending date (YYJJJ)
     * etime - Ending hour (HHMM)
     * mspec - Species names for nspec species (character*4(10,nspec) array)
     * val - Species l, layer k initial concentrations (ppm for gases, ug/m3 for aerosols)
     *       for nx grid columns and ny grid rows
     *
     */
    // CAMx UAM-IV Files are all big endian
    raf.order(RandomAccessFile.BIG_ENDIAN);

    // Prepare an array for binary data
    int size = (int) v2.getSize();
    float[] arr = new float[size];

    // Move to data block of file
    raf.seek(this.data_start);

    /*
     * First record is stime,sdate,etime,edate
     * We are skipping the data, but checking
     * the consistency of the Fortran "unformatted"
     * data record
    */
    int pad1 = raf.readInt();
    raf.skipBytes(16);
    int pad2 = raf.readInt();
    if (pad1 != pad2) {
      throw new IOException("Asymmetric fortran buffer values: 1");
    }

    // Find species name/id associated with this variable
    int spcid = -1;
    String spc = "";
    while (!spc.equals(v2.getShortName())) {
      spc = this.species_names[++spcid];
    }

    /*
    * Skip data associated with species that are prior
    * in the data block
    */
    raf.skipBytes(this.spc_3D_block * spcid * 4);

    // Initialize count for indexing arr
    int count = 0;

    while (count < size) {

      /*
      * Read species name and store the initial record pad.
      * Note: it might be good to compare
      *       spc string to variable.getShortName
      */
      if (count == 0) {
        pad1 = raf.readInt();
        int ione = raf.readInt();
        spc = raf.readString(40);
      }

      /*
      * If we have read a 2D slice, read the final record pad
      * and compare to initial record pad.  If everything is okay, proceed.
      * (1) skip to next 2D slice
      * (2) store initial pad
      * (3) read spc name
      * Note: it might be good to compare
      *       spc string to variable.getShortName
      */
      if ((count != 0) && ((count % this.n2dvals) == 0)) {
        pad2 = raf.readInt();
        if (pad1 != pad2) {
          //System.out.println(pad1);
          //System.out.println(pad2);
          throw new IOException("Asymmetric fortran buffer values: 2");
        }
        if ((count % this.n3dvals) == 0) {
          raf.skipBytes((this.data_block - this.spc_3D_block) * 4);
        }
        pad1 = raf.readInt();
        int ione = raf.readInt();
        spc = raf.readString(40);
      }

      /*
      * Attempt to read a Float from the file
      */
      try {
        arr[count++] = raf.readFloat();
      } catch (java.lang.ArrayIndexOutOfBoundsException io) {
        throw new IOException(io.getMessage());
      }
    }

    // Convert java float[] to ma2.Array
    Array data = Array.factory(DataType.FLOAT, v2.getShape(), arr);

    // Subset the data based on the wantSection and return a 4D variable
    return data.sectionNoReduce(wantSection.getRanges());
  }

}


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

package ucar.nc2.ft.point.standard;

import ucar.nc2.constants.CF;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.constants.FeatureType;
import ucar.nc2.Variable;
import ucar.nc2.Dimension;
import ucar.nc2.Structure;
import ucar.nc2.Attribute;
import ucar.ma2.DataType;

import java.util.Formatter;

/**
 * Helper routines for Nested Tables
 *
 * @author caron
 * @since Apr 23, 2008
 */
public abstract class Evaluator {
  public static class VarAtt {
    public Variable var;
    public Attribute att;

    public VarAtt(Variable var, Attribute att) {
      this.var = var;
      this.att = att;
    }
  }

  static public String findNameVariableWithStandardNameAndDimension(NetcdfDataset ds, String standard_name, Dimension outer, Formatter errlog) {
    Variable v = findVariableWithAttributeAndDimension(ds, CF.STANDARD_NAME, standard_name, outer, errlog);
    return (v == null) ? null : v.getShortName();
  }

  static public Variable findVariableWithAttributeAndDimension(NetcdfDataset ds, String att_name, String att_value, Dimension outer, Formatter errlog) {
    for (Variable v : ds.getVariables()) {
      String attValue = ds.findAttValueIgnoreCase(v, att_name, null);
      if ((attValue != null) && attValue.equalsIgnoreCase(att_value)) {
        if (v.getRank() > 0 && v.getDimension(0).equals(outer))
          return v;
        if (isEffectivelyScaler(v) && (outer == null))
          return v;
      }
    }

     // descend into structures
    for (Variable v : ds.getVariables()) {
      if (v instanceof Structure) {
        Structure s = (Structure) v;
        if (s.getRank() > 0 && s.getDimension(0).equals(outer) || (s.getRank() == 0 && outer == null)) {
          for (Variable vs : s.getVariables()) {
            Attribute att = vs.findAttributeIgnoreCase(att_name);
            if ((att != null) && att.isString() && att.getStringValue().equalsIgnoreCase(att_value))
              return vs;
          }
        }
      }
    }

    // failed
    return null;
  }

  static public boolean isEffectivelyScaler(Variable v) {
    return (v.getRank() == 0) || (v.getRank() == 1 && v.getDataType() == DataType.CHAR);
  }

  /**
   * Find first variable with given attribute name
   *
   * @param ds      in this dataset
   * @param attName attribute name, case insensitive
   * @return first variable with given attribute name, or null
   */
  static public VarAtt findVariableWithAttribute(NetcdfDataset ds, String attName) {
    for (Variable v : ds.getVariables()) {
      Attribute att = v.findAttributeIgnoreCase(attName);
      if (att != null) return new VarAtt(v, att);
    }

    // descend into structures
    for (Variable v : ds.getVariables()) {
      if (v instanceof Structure) {
        Structure s = (Structure) v;
        for (Variable vs : s.getVariables()) {
          Attribute att = vs.findAttributeIgnoreCase(attName);
          if (att != null) return new VarAtt(vs, att);
        }
      }
    }
    return null;
  }

  /**
   * Find first variable with given attribute name and value.
   * If not found, search one level into structures.
   *
   * @param ds       in this dataset
   * @param attName  attribute name, case insensitive
   * @param attValue attribute value, case sensitive
   * @return first variable with given attribute name and value, or null
   */
  static public Variable findVariableWithAttributeValue(NetcdfDataset ds, String attName, String attValue) {
    for (Variable v : ds.getVariables()) {
      String haveValue = ds.findAttValueIgnoreCase(v, attName, null);
      if ((haveValue != null) && haveValue.equals(attValue))
        return v;
    }

    // descend into structures
    for (Variable v : ds.getVariables()) {
      if (v instanceof Structure) {
        Variable vn = findVariableWithAttributeValue((Structure) v, attName, attValue);
        if (null != vn) return vn;
      }
    }
    return null;
  }

  /**
   * Find first variable with given attribute name and value
   *
   * @param ds       in this dataset
   * @param attName  attribute name, case insensitive
   * @param attValue attribute value, case sensitive
   * @return name of first variable with given attribute name and value, or null
   */
  static public String findNameOfVariableWithAttributeValue(NetcdfDataset ds, String attName, String attValue) {
    Variable v = findVariableWithAttributeValue(ds, attName, attValue);
    return (v == null) ? null : v.getShortName();
  }

  /**
   * Find first member variable in this struct with given attribute name and value
   *
   * @param struct   in this structure
   * @param attName  attribute name, case insensitive
   * @param attValue attribute value, case sensitive
   * @return first member variable with given attribute name and value, or null
   */
  static public Variable findVariableWithAttributeValue(Structure struct, String attName, String attValue) {
    for (Variable v : struct.getVariables()) {
      Attribute att = v.findAttributeIgnoreCase(attName);
      if ((att != null) && att.getStringValue().equals(attValue))
        return v;
    }
    return null;
  }

  /**
   * Find structure variable of rank 2 with the 2 given dimensions
   * (or) Find structure variable of rank 1 with the 1 given dimension
   * @param ds  in this dataset
   * @param dim0 first dimension
   * @param dim1 second dimension  (ok to be null)
   * @return structure variable or null
   */
  static public Structure findStructureWithDimensions(NetcdfDataset ds, Dimension dim0, Dimension dim1) {
    for (Variable v : ds.getVariables()) {
      if (!(v instanceof Structure)) continue;

      if (dim1 != null && v.getRank() == 2 && v.getDimension(0).equals(dim0) && v.getDimension(1).equals(dim1))
        return (Structure) v;

      if (dim1 == null && v.getRank() == 1 && v.getDimension(0).equals(dim0))
         return (Structure) v;
      }
  return null;
  }

  /**
   * Find first nested structure
   * @param s in this structure
   * @return first nested structure or null
   */
  static public Structure findNestedStructure(Structure s) {
    for (Variable v : s.getVariables()) {
      if ((v instanceof Structure))
        return (Structure) v;
    }
    return null;
  }

  /**
   * Does this dataset have a record structure? netcdf-3 specific
   * @param ds in this dataset
   * @return true if record structure exists
   */
  static public boolean hasNetcdf3RecordStructure(NetcdfDataset ds) {
    Variable v = ds.findVariable("record");
    return (v != null) && (v.getDataType() == DataType.STRUCTURE);
  }

  ////////////////////////////////////////////////////////////////////////////
  // literals support ":gatt"

  /**
   * Translate key to value
   *
   * @param ds     search in this dataset
   * @param key    if starts with ":", search for global attribute
   * @param errlog error messages here
   * @return return global attribute value or the key itself
   */
  static public String getLiteral(NetcdfDataset ds, String key, Formatter errlog) {
    if (key.startsWith(":")) {
      String val = ds.findAttValueIgnoreCase(null, key.substring(1), null);
      if ((val == null) && (errlog != null))
        errlog.format(" Cant find global attribute %s%n", key);
      return val;
    }

    return key;
  }

  /**
   * Turn the key into a String and return the corresponding featureType, if any.
   *
   * @param ds     look in this datset
   * @param key    if starts with ":", replace with value of global attribute
   * @param errlog error messages here
   * @return featureType, or null
   */
  static public FeatureType getFeatureType(NetcdfDataset ds, String key, Formatter errlog) {
    FeatureType ft = null;
    String fts = getLiteral(ds, key, errlog);
    if (fts != null) {
      ft = FeatureType.valueOf(fts.toUpperCase());
      if ((ft == null) && (errlog != null))
        errlog.format(" Cant find Feature type %s from %s%n", fts, key);
    }
    return ft;
  }

  /**
   * Find the variable pointed to by key
   *
   * @param ds     in this dataset
   * @param key    may be variable name or ":gatt" where gatt is local attribute whose value is the variable name
   * @param errlog error messages here
   * @return name of variable or null if not exist
   */
  static public String getVariableName(NetcdfDataset ds, String key, Formatter errlog) {
    Variable v = null;
    String vs = getLiteral(ds, key, errlog);
    if (vs != null) {
      v = ds.findVariable(vs);
      if ((v == null) && (errlog != null))
        errlog.format(" Cant find Variable %s from %s%n", vs, key);
    }
    return v == null ? null : v.getShortName();
  }

  /**
   * Find the dimension pointed to by key
   *
   * @param ds     in this dataset
   * @param key    may be dimension name or ":gatt" where gatt is local attribute whose value is the dimension name
   * @param errlog error messages here
   * @return dimension or null if not exist
   */
  static public Dimension getDimension(NetcdfDataset ds, String key, Formatter errlog) {
    Dimension d = null;
    String s = getLiteral(ds, key, errlog);
    if (s != null) {
      d = ds.findDimension(s); // LOOK use group
      if ((d == null) && (errlog != null))
        errlog.format(" Cant find Variable %s from %s%n", s, key);
    }
    return d;
  }

  /**
   * Find the dimension pointed to by key
   *
   * @param ds     in this dataset
   * @param key    may be dimension name or ":gatt" where gatt is local attribute whose value is the dimension name
   * @param errlog error messages here
   * @return name of dimension or null if not exist
   */
  static public String getDimensionName(NetcdfDataset ds, String key, Formatter errlog) {
    Dimension d = getDimension(ds, key, errlog);
    return (d == null) ? null : d.getShortName();
  }

  private Evaluator() { }  // Private ctor to ensure non-instantiability.
}


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

import ucar.ma2.*;
import ucar.nc2.Attribute;
import ucar.nc2.Variable;
import ucar.nc2.constants.AxisType;
import ucar.nc2.constants.CF;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.util.Misc;

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

/**
 * A 2-dimensional numeric Coordinate Axis. Must be invertible meaning, roughly, that
 *   if you draw lines connecting the points, none would cross.
 *
 * @see CoordinateAxis#factory
 * @author john caron
 */

public class CoordinateAxis2D extends CoordinateAxis {
  static private org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CoordinateAxis2D.class);
  static private final boolean debug = false;

  private ArrayDouble.D2 coords = null;  // LOOK maybe optional for large arrays, or maybe eliminate all together, and read each time ??

  /**
   * Create a 2D coordinate axis from an existing VariableDS
   *
   * @param ncd the containing dataset
   * @param vds create it from here
   */
  public CoordinateAxis2D(NetcdfDataset ncd, VariableDS vds) {
    super(ncd, vds);
    isContiguous = false;
  }

  // for section and slice
  @Override
  protected Variable copy() {
    return new CoordinateAxis2D(this.ncd, this);
  }

  /**
   * Get the coordinate value at the i, j index.
   *
   * @param i index 0 (fastest varying, right-most)
   * @param j index 1
   * @return midpoint.get(j, i).
   */
  public double getCoordValue(int j, int i) {
    if (coords == null) doRead();
    return coords.get(j, i);
  }

  private void doRead() {
    Array data;
    try {
      data = read();
      // if (!hasCachedData()) setCachedData(data, false); //cache data for subsequent reading
    } catch (IOException ioe) {
      log.error("Error reading coordinate values " + ioe);
      throw new IllegalStateException(ioe);
    }

    if (data.getRank() != 2)
      throw new IllegalArgumentException("must be 2D");
    if (debug)
      System.out.printf("Coordinate2D read%n");

    coords = (ArrayDouble.D2) Array.factory(DataType.DOUBLE, data.getShape(), data.get1DJavaArray(DataType.DOUBLE));

    if (this.axisType == AxisType.Lon)
      makeConnectedLon(coords);
  }

  private boolean isInterval;
  private boolean intervalWasComputed;

  public boolean isInterval() {
    if (!intervalWasComputed)
      isInterval = computeIsInterval();
    return isInterval;
  }

  private void makeConnectedLon(ArrayDouble.D2 mid) {

    int[] shape = mid.getShape();
    int ny = shape[0];
    int nx = shape[1];

    // first row
    double connect = mid.get(0, 0);
    for (int i = 1; i < nx; i++) {
      connect = connectLon(connect, mid.get(0, i));
      mid.set(0, i, connect);
    }

    // other rows
    for (int j = 1; j < ny; j++) {
      connect = mid.get(j - 1, 0);
      for (int i = 0; i < nx; i++) {
        connect = connectLon(connect, mid.get(j, i));
        mid.set(j, i, connect);
      }
    }

  }

  private static final double MAX_JUMP = 100.0; // larger than you would ever expect

  static private double connectLon(double connect, double val) {
    if (Double.isNaN(connect)) return val;
    if (Double.isNaN(val)) return val;

    double diff = val - connect;
    if (Math.abs(diff) < MAX_JUMP) return val; // common case fast
    // we have to add or subtract 360
    double result = diff > 0 ? val - 360 : val + 360;
    double diff2 = connect - result;
    if ((Math.abs(diff2)) < Math.abs(diff))
      val = result;
    return val;
  }

  /**
   * Get the coordinate values as a 1D double array, in canonical order.
   *
   * @return coordinate values
   * @throws UnsupportedOperationException if !isNumeric()
   */
  public double[] getCoordValues() {
    if (coords == null) doRead();
    if (!isNumeric())
      throw new UnsupportedOperationException("CoordinateAxis2D.getCoordValues() on non-numeric");
    return (double[]) coords.get1DJavaArray(DataType.DOUBLE);
  }

  /**
   * Create a new CoordinateAxis2D as a section of this CoordinateAxis2D.
   *
   * @param r1 the section on the first index
   * @param r2 the section on the second index
   * @return a section of this CoordinateAxis2D
   * @throws InvalidRangeException if specified Ranges are invalid
   */
  public CoordinateAxis2D section(Range r1, Range r2) throws InvalidRangeException {
    List<Range> section = new ArrayList<>();
    section.add(r1);
    section.add(r2);
    return (CoordinateAxis2D) section(section);
  }

  public ArrayDouble.D2 getCoordValuesArray() {
    if (coords == null) doRead();
    return coords;
  }

  /**
   * Only call if isInterval()
   *
   * @return bounds array pr null if not an interval
   */
  public ArrayDouble.D3 getCoordBoundsArray() {
    if (coords == null) doRead();
    return makeBoundsFromAux();
  }

  public ArrayDouble.D2 getEdges() {
    ArrayDouble.D2 mids = getCoordValuesArray();
    return makeEdges(mids);
  }

  /**
   * @deprecated use getEdges()
   */
  public ArrayDouble.D2 getXEdges() {
    ArrayDouble.D2 mids = getCoordValuesArray();
    return makeEdges(mids);
  }

  /**
   * @deprecated use getEdges()
   */
  public ArrayDouble.D2 getYEdges() {
    ArrayDouble.D2 mids = getCoordValuesArray();
    return makeEdges(mids);
  }

  /**
   * Normal case: do something reasonable in deciding on the edges when we have the midpoints of a 2D coordinate.
   *
   * @param midpoints values of midpoints with shape (ny, nx)
   * @return values of edges with shape (ny+1, nx+1)
   */
  static public ArrayDouble.D2 makeEdges(ArrayDouble.D2 midpoints) {
    int[] shape = midpoints.getShape();
    int ny = shape[0];
    int nx = shape[1];
    ArrayDouble.D2 edge = new ArrayDouble.D2(ny + 1, nx + 1);

    for (int y = 0; y < ny - 1; y++) {
      for (int x = 0; x < nx - 1; x++) {
        // the interior edges are the average of the 4 surrounding midpoints
        double xval = (midpoints.get(y, x) + midpoints.get(y, x + 1) + midpoints.get(y + 1, x) + midpoints.get(y + 1, x + 1)) / 4;
        edge.set(y + 1, x + 1, xval);
      }
      // extrapolate to exterior points
      edge.set(y + 1, 0, edge.get(y + 1, 1) - (edge.get(y + 1, 2) - edge.get(y + 1, 1)));
      edge.set(y + 1, nx, edge.get(y + 1, nx - 1) + (edge.get(y + 1, nx - 1) - edge.get(y + 1, nx - 2)));
    }

    // extrapolate to the first and last row
    for (int x = 0; x < nx + 1; x++) {
      edge.set(0, x, edge.get(1, x) - (edge.get(2, x) - edge.get(1, x)));
      edge.set(ny, x, edge.get(ny - 1, x) + (edge.get(ny - 1, x) - edge.get(ny - 2, x)));
    }

    return edge;
  }

  /**
   * Experimental: for WRF rotated (NMM "E") Grids
   *
   * @param midx x coordinates of midpoints
   * @return x coordinates of edges with shape (ny+2, nx+1)
   */
  static public ArrayDouble.D2 makeXEdgesRotated(ArrayDouble.D2 midx) {
    int[] shape = midx.getShape();
    int ny = shape[0];
    int nx = shape[1];
    ArrayDouble.D2 edgex = new ArrayDouble.D2(ny + 2, nx + 1);

    // compute the interior rows
    for (int y = 0; y < ny; y++) {
      for (int x = 1; x < nx; x++) {
        double xval = (midx.get(y, x - 1) + midx.get(y, x)) / 2;
        edgex.set(y + 1, x, xval);
      }
      edgex.set(y + 1, 0, midx.get(y, 0) - (edgex.get(y + 1, 1) - midx.get(y, 0)));
      edgex.set(y + 1, nx, midx.get(y, nx - 1) - (edgex.get(y + 1, nx - 1) - midx.get(y, nx - 1)));
    }

    // compute the first row
    for (int x = 0; x < nx; x++) {
      edgex.set(0, x, midx.get(0, x));
    }

    // compute the last row
    for (int x = 0; x < nx - 1; x++) {
      edgex.set(ny + 1, x, midx.get(ny - 1, x));
    }

    return edgex;
  }

  /**
   * Experimental: for WRF rotated (NMM "E") Grids
   *
   * @param midy y coordinates of midpoints
   * @return y coordinates of edges with shape (ny+2, nx+1)
   */
  static public ArrayDouble.D2 makeYEdgesRotated(ArrayDouble.D2 midy) {
    int[] shape = midy.getShape();
    int ny = shape[0];
    int nx = shape[1];
    ArrayDouble.D2 edgey = new ArrayDouble.D2(ny + 2, nx + 1);

    // compute the interior rows
    for (int y = 0; y < ny; y++) {
      for (int x = 1; x < nx; x++) {
        double yval = (midy.get(y, x - 1) + midy.get(y, x)) / 2;
        edgey.set(y + 1, x, yval);
      }
      edgey.set(y + 1, 0, midy.get(y, 0) - (edgey.get(y + 1, 1) - midy.get(y, 0)));
      edgey.set(y + 1, nx, midy.get(y, nx - 1) - (edgey.get(y + 1, nx - 1) - midy.get(y, nx - 1)));
    }

    // compute the first row
    for (int x = 0; x < nx; x++) {
      double pt0 = midy.get(0, x);
      double pt = edgey.get(2, x);

      double diff = pt0 - pt;
      edgey.set(0, x, pt0 + diff);
    }

    // compute the last row
    for (int x = 0; x < nx - 1; x++) {
      double pt0 = midy.get(ny - 1, x);
      double pt = edgey.get(ny - 1, x);

      double diff = pt0 - pt;
      edgey.set(ny + 1, x, pt0 + diff);
    }

    return edgey;
  }

  ///////////////////////////////////////////////////////////////////////////////
  // bounds calculations

  private ArrayDouble.D3 makeBoundsFromAux() {
    if (!computeIsInterval()) return null;

    Attribute boundsAtt = findAttributeIgnoreCase(CF.BOUNDS);
    if (boundsAtt == null) return null;

    String boundsVarName = boundsAtt.getStringValue();
    VariableDS boundsVar = (VariableDS) ncd.findVariable(getParentGroup(), boundsVarName);

    Array data;
    try {
      //boundsVar.setUseNaNs(false); // missing values not allowed
      data = boundsVar.read();
    } catch (IOException e) {
      log.warn("CoordinateAxis2D.makeBoundsFromAux read failed ", e);
      return null;
    }

    ArrayDouble.D3 bounds;
    assert (data.getRank() == 3) && (data.getShape()[2] == 2) : "incorrect shape data for variable " + boundsVar;
    if (data instanceof ArrayDouble.D3) {
      bounds = (ArrayDouble.D3) data;
    } else {
      bounds = (ArrayDouble.D3) Array.factory(DataType.DOUBLE, data.getShape());
      MAMath.copy(data, bounds);
    }

    return bounds;
  }

  private boolean computeIsInterval() {
    intervalWasComputed = true;

    Attribute boundsAtt = findAttributeIgnoreCase(CF.BOUNDS);
    if ((null == boundsAtt) || !boundsAtt.isString()) return false;
    String boundsVarName = boundsAtt.getStringValue();
    VariableDS boundsVar = (VariableDS) ncd.findVariable(getParentGroup(), boundsVarName);
    if (null == boundsVar) return false;
    if (3 != boundsVar.getRank()) return false;
    if (getDimension(0) != boundsVar.getDimension(0)) return false;
    if (getDimension(1) != boundsVar.getDimension(1)) return false;
    return 2 == boundsVar.getDimension(2).getLength();
  }

  ///////////////////////////////////////
  // time

  public CoordinateAxisTimeHelper getCoordinateAxisTimeHelper() {
    return new CoordinateAxisTimeHelper(getCalendarFromAttribute(), getUnitsString());
  }

  public int findTimeIndexFromCalendarDate(int run_idx, CalendarDate want) throws IOException, InvalidRangeException {
    CoordinateAxisTimeHelper helper = getCoordinateAxisTimeHelper();
    double wantOffset = helper.offsetFromRefDate(want);

    if (isInterval()) {
      ArrayDouble.D3 bounds = getCoordBoundsArray();
      if (bounds == null)
        throw new IllegalStateException("getCoordBoundsArray returned null for coordinate "+getFullName());
      ArrayDouble.D2 boundsForRun = (ArrayDouble.D2) bounds.slice(0,run_idx );

      int idx = findSingleHit(boundsForRun, wantOffset);
      if (idx >= 0) return idx;
      if (idx == -1) return -1;
      // multiple hits = choose closest to the midpoint
      return findClosest(boundsForRun, wantOffset);

    } else {
      ArrayDouble.D2 values = getCoordValuesArray();
      ArrayDouble.D1 valuesForRun = (ArrayDouble.D1) values.slice(0,run_idx );
      for (int i=0; i<valuesForRun.getSize(); i++) {
        if (Misc.nearlyEquals(valuesForRun.get(i), wantOffset))
          return i;
      }
      return -1;
    }
  }

  // return index if only one match, if no matches return -1, if > 1 match return -nhits
  private int findSingleHit(ArrayDouble.D2 boundsForRun, double target) {
    int hits = 0;
    int idxFound = -1;
    int n = boundsForRun.getShape()[0];
    for (int i = 0; i < n; i++) {
      if (contains(target, boundsForRun.get(i,0), boundsForRun.get(i,1))) {
        hits++;
        idxFound = i;
      }
    }
    if (hits == 1) return idxFound;
    if (hits == 0) return -1;
    return -hits;
  }

  // return index of closest value to target
  private int findClosest(ArrayDouble.D2 boundsForRun , double target) {
    double minDiff =  Double.MAX_VALUE;
    int idxFound = -1;
    int n = boundsForRun.getShape()[0];
    for (int i = 0; i < n; i++) {
      double midpoint = (boundsForRun.get(i,0) + boundsForRun.get(i,1))/2.0;
      double diff =  Math.abs(midpoint - target);
      if (diff < minDiff) {
        minDiff = diff;
        idxFound = i;
      }
    }
    return idxFound;
  }

  private boolean contains(double target, double b1, double b2) {
    if (b1 <= target && target <= b2) return true;
    return b1 >= target && target >= b2;
  }

}


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

import ucar.unidata.util.StringUtil2;

import javax.annotation.concurrent.ThreadSafe;
import java.io.File;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.Formatter;
import java.util.regex.Pattern;

/**
 * Parses the collection specification string.
 *
the idea  is that one copies the full path of an example dataset, then edits it

 *
Example: "/data/ldm/pub/native/grid/NCEP/GFS/Alaska_191km/** /GFS_Alaska_191km_#yyyyMMdd_HHmm#\.grib1$"

 *

 *
 rootDir ="/data/ldm/pub/native/grid/NCEP/GFS/Alaska_191km"/

 *
    subdirs=true (because ** is present)

 *
    dateFormatMark="GFS_Alaska_191km_#yyyyMMdd_HHmm"

 *
    regExp='GFS_Alaska_191km_.............\.grib1$

 *

 *
Example: "Q:/grid/grib/grib1/data/agg/.*\.grb"

 *

 *
 rootDir ="Q:/grid/grib/grib1/data/agg/"/

 *
    subdirs=false

 *
    dateFormatMark=null

 *
    useName=yes

 *
    regexp= ".*\.grb" (anything ending with .grb)

 *

 *
 * @see "http://www.unidata.ucar.edu/projects/THREDDS/tech/tds4.2/reference/collections/CollectionSpecification.html"
 * @author caron
 * @since Jul 7, 2009
 */
@ThreadSafe
public class CollectionSpecParser {
  private final String spec;
  private final String rootDir;
  private final boolean subdirs; // recurse into subdirectories under the root dir
  private final boolean filterOnName; // filter on name, else on entire path
  private final java.util.regex.Pattern filter; // regexp filter
  private final String dateFormatMark;

  /**
   * Single spec : "/topdir/** /#dateFormatMark#regExp"
   * This only allows the dateFormatMark to be in the file name, not anywhere else in the filename path,
   *  and you cant use any part of the dateFormat to filter on.
   * @param collectionSpec the collection Spec
   * @param errlog put error messages here, may be null
   */
  public CollectionSpecParser(String collectionSpec, Formatter errlog) {
    this.spec = collectionSpec.trim();
    int posFilter;

    int posGlob = collectionSpec.indexOf("/**/");
    if (posGlob > 0) {
      rootDir = collectionSpec.substring(0, posGlob);
      posFilter = posGlob + 3;
      subdirs = true;

    } else {
      subdirs = false;
      posFilter = collectionSpec.lastIndexOf('/');
      if (posFilter > 0)
        rootDir = collectionSpec.substring(0, posFilter);
      else
        rootDir = System.getProperty("user.dir"); // working directory
    }

    File locFile = new File(rootDir);
    if (!locFile.exists() && errlog != null) {
      errlog.format(" Directory %s does not exist %n", rootDir);
    }

    // optional filter
    String filter = null;
    if (posFilter < collectionSpec.length() - 2)
      filter = collectionSpec.substring(posFilter + 1); // remove topDir

    if (filter != null) {
      // optional dateFormatMark
      int posFormat = filter.indexOf('#');
      if (posFormat >= 0) {
        // check for two hash marks
        int posFormat2 = filter.lastIndexOf('#');

        if (posFormat != posFormat2) { // two hash
          dateFormatMark = filter.substring(0, posFormat2); // everything up to the second hash
          filter = StringUtil2.remove(filter, '#'); // remove hashes, replace with .
          StringBuilder sb = new StringBuilder(filter);
          for (int i = posFormat; i < posFormat2 - 1; i++)
            sb.setCharAt(i, '.');
          String regExp = sb.toString();
          this.filter = java.util.regex.Pattern.compile(regExp);

        } else { // one hash
          dateFormatMark = filter; // everything
          String regExp = filter.substring(0, posFormat) + "*";
          this.filter = java.util.regex.Pattern.compile(regExp);
        }

      } else { // no hash (dateFormatMark)
        dateFormatMark = null;
        this.filter = java.util.regex.Pattern.compile(filter);
      }
    } else {
      dateFormatMark = null;
      this.filter = null;
    }

    this.filterOnName = true;
  }

  public CollectionSpecParser(String rootDir, String regExp, Formatter errlog) {
    this.rootDir = StringUtil2.removeFromEnd(rootDir, '/');
    this.subdirs = true;
    this.spec = this.rootDir +"/" + regExp;
    this.filter = java.util.regex.Pattern.compile(spec);
    this.dateFormatMark = null;
    this.filterOnName = false;
  }

  public PathMatcher getPathMatcher() {
    if (spec.startsWith("regex:") || spec.startsWith("glob:")) {  // experimental
      return FileSystems.getDefault().getPathMatcher(spec);
    } else {
      return new BySpecp();
    }
  }

  private class BySpecp implements java.nio.file.PathMatcher {
    @Override
    public boolean matches(Path path) {
      java.util.regex.Matcher matcher = filter.matcher(path.getFileName().toString());
      return matcher.matches();
    }
  }

  public String getRootDir() {
    return rootDir;
  }

  public boolean wantSubdirs() {
    return subdirs;
  }

  public Pattern getFilter() {
    return filter;
  }

  public boolean getFilterOnName() {
    return filterOnName;
  }

  public String getDateFormatMark() {
    return dateFormatMark;
  }

  @Override
  public String toString() {
    return "CollectionSpecParser{" +
            "\n   topDir='" + rootDir + '\'' +
            "\n   subdirs=" + subdirs +
            "\n   regExp='" + filter + '\'' +
            "\n   dateFormatMark='" + dateFormatMark + '\'' +
  //          "\n   useName=" + useName +
            "\n}";
  }

  /////////////////////////////////////////////////////////
  // debugging

  /* private static void doit2(String spec, String timePart, Formatter errlog) {
    CollectionSpecParser specp = new CollectionSpecParser(spec, timePart, errlog);
    System.out.printf("spec= %s timePart=%s%n%s%n", spec, timePart, specp);
    String err = errlog.toString();
    if (err.length() > 0)
      System.out.printf("%s%n", err);
    System.out.printf("-----------------------------------%n");
  }

  public static void main(String arg[]) {
    doit2("G:/nomads/cfsr/timeseries/** /.*grb2$", "G:/nomads/cfsr/#timeseries/#yyyyMM", new Formatter());
    //doit("C:/data/formats/gempak/surface/#yyyyMMdd#_sao\\.gem", new Formatter());
    // doit("Q:/station/ldm/metar/Surface_METAR_#yyyyMMdd_HHmm#.nc", new Formatter());
  }  */

  private static void doit(String spec, Formatter errlog) {
    CollectionSpecParser specp = new CollectionSpecParser(spec, errlog);
    System.out.printf("spec= %s%n%s%n", spec, specp);
    String err = errlog.toString();
    if (err.length() > 0)
      System.out.printf("%s%n", err);
    System.out.printf("-----------------------------------%n");
  }

  public static void main(String arg[]) {
    doit("/u00/FNMOC/NAVGEM/pressure/**/US058GMET-GR1mdl.0018_0056_00000F0..#yyyyMMddHH#_0102_000000-000000pres$", new Formatter());

    doit("/data/ldm/pub/native/grid/NCEP/GFS/Alaska_191km/**/GFS_Alaska_191km_#yyyyMMdd_HHmm#\\.grib1$", new Formatter());
    doit("Q:/grid/grib/grib1/data/agg/.*\\.grb", new Formatter());
    doit("/data/ldm/pub/decoded/netcdf/surface/metar/**/Surface_METAR_#yyyyMMdd_HHmm#\\.nc", new Formatter());
    doit("/data/ldm/pub/decoded/netcdf/surface/metar/**/Surface_METAR_#yyyyMMdd_HHmm#.nc", new Formatter());
    doit("/data/ldm/pub/decoded/netcdf/surface/metar/**/Surface_METAR_#yyyyMMdd_HHmm", new Formatter());
    doit("/data/ldm/pub/decoded/netcdf/surface/metar/Surface_METAR_#yyyyMMdd_HHmm", new Formatter());
    doit("/data/ldm/pub/decoded/netcdf/surface/metar/Surface_METAR_#yyyyMMdd_HHmm#.nc", new Formatter());
    doit("/data/ldm/pub/decoded/netcdf/surface/metar/Surface_METAR_yyyyMMdd_HHmm.nc", new Formatter());
    doit("/data/ldm/pub/decoded/netcdf/surface/metar/", new Formatter());
    doit("/data/ldm/pub/decoded/netcdf/surface/metar/**/", new Formatter());
    doit("/data/ldm/pub/decoded/netcdf/surface/metar/**/*", new Formatter());
    doit("/data/ldm/pub/decoded/netcdf/surface/metar/*", new Formatter());
    doit("/data/ldm/pub/decoded/netcdf/surface/metar/T*.T", new Formatter());
  }

}


/*
 * 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 managing a UnitSystem.
 *
 * @author Steven R. Emmerson
 */
public final class UnitSystemManager implements Serializable {
	private static final long	serialVersionUID	= 1L;

	/**
	 * The singleton instance of the system of units.
	 *
	 * @serial
	 */
	private static UnitSystem	instance;

	/**
	 * Returns an instance of the system of units.
	 *
	 * @return An instance of the system of units.
	 */
	public static synchronized UnitSystem instance() throws UnitSystemException {
			if (instance == null) {
				instance = SI.instance();
		}
		return instance;
	}

	/**
	 * Sets the system of units. This must be called before any call to
	 * instance().
	 *
	 * @param instance
	 *            The system of units.
	 * @throws UnitSystemException
	 *             instance() was called earlier.
	 */
	public static synchronized void setInstance(final UnitSystem instance)
			throws UnitSystemException {
		if (instance != null) {
			throw new UnitSystemException("Unit system already used");
		}
		UnitSystemManager.instance = instance;
	}
}


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

import ucar.nc2.dataset.StructureDS;
import ucar.nc2.dataset.VariableDS;
import ucar.nc2.util.Indent;

import java.util.HashMap;
import java.util.Map;

/**
 * Define a superclass for all the CDM node classes: Group, Dimension, etc.
 * Define the sort of the node {@link CDMSort} so that we can
 * 1. do true switching on node type
 * 2. avoid use of instanceof
 * 3. Use container classes that have more than one kind of node
 * <p>
 * Also move various common fields and methods to here.
 *
 * @author Heimbigner
 */

public abstract class CDMNode
{

    protected CDMSort sort = null;
    protected Group group = null;
    protected Structure parentstruct = null;
    protected boolean immutable = false;
    protected String shortName = null;

    protected Map<Object,Object> annotations = null;

    //String fullName = null; // uses backslash escaping

    // HACK: sadly, because of the existing class structure,
    // we need to track the original name of a
    // Variable/Dimension/Attribute object.
    // This is primarily used in DAP processing
    // because the names in the DAP DDS and/or DAS
    // may contain group information separated
    // by forward slash.

    protected String dodsname = null;

    //////////////////////////////////////////////////
    // Constructors

    protected CDMNode()
    {
        // Use Instanceof to figure out the sort
        if(this instanceof Attribute)
            setSort(CDMSort.ATTRIBUTE);
        else if(this instanceof Dimension)
            setSort(CDMSort.DIMENSION);
        else if(this instanceof EnumTypedef)
            setSort(CDMSort.ENUMERATION);
        else if(this instanceof Sequence)
            setSort(CDMSort.SEQUENCE);
        else if(this instanceof Structure)
            setSort(CDMSort.STRUCTURE);
        else if(this instanceof Group)
            setSort(CDMSort.GROUP);
        else if(this instanceof Variable) // Only case left is atomic var
            setSort(CDMSort.VARIABLE);
    }

    public CDMNode(String name)
    {
        this();
        setShortName(name);
    }

    // Get/Set
    public CDMSort getSort()
    {
        return this.sort;
    }

    public void setSort(CDMSort sort)
    {
        if(!immutable) this.sort = sort;
    }

    /**
     * Get the short name of this Variable. The name is unique within its parent group.
     */
    public String getShortName()
    {
        return this.shortName;
    }

    /**
     * Set the short name of this Variable. The name is unique within its parent group.
     *
     * @param name new short name
     */
    public void setShortName(String name)
    {
        if(!immutable) this.shortName = NetcdfFile.makeValidCdmObjectName(name);
    }

    /**
     * Get its parent Group, or null if its the root group.
     *
     * @return parent Group
     */
    public Group getParentGroup()
    {
        return this.group;
    }

    /**
     * Alias for getParentGroup
     *
     * @return parent Group
     */
    public Group getGroup()
    {
        return getParentGroup();
    }

    /**
     * Set the parent Group
     *
     * @param parent The new parent group
     */
    public void setParentGroup(Group parent)
    {
        if(!immutable) this.group = parent;
    }

    /**
     * Get its parent structure, or null if not in structure
     *
     * @return parent structure
     */
    public Structure getParentStructure()
    {
        return this.parentstruct;
    }

    /**
     * Set the parent Structure
     *
     * @param parent The new parent structure
     */
    public void setParentStructure(Structure parent)
    {
        if(!immutable) this.parentstruct = parent;
    }

    /**
     * Test for presence of parent Structure
     *
     * @return true iff struct != null
     */

    public boolean isMemberOfStructure()
    {
        return this.parentstruct != null;
    }

    /**
     * Get immutable flag
     * As a rule, subclasses will access directly
     *
     * @return Immutable flag
     */
    public boolean getImmutable()
    {
        return this.immutable;
    }

    /**
     * Set the immutable flag to true.
     * Once set, cannot be unset
     */
    public CDMNode setImmutable()
    {
        this.immutable = true;
        return this;
    }

    /**
     * Get the dodsname
     *
     * @return the original names from the DDS or DAS; if null,
     * then return the short name
     */
    public String getDODSName()
    {
        if(dodsname == null)
            return this.shortName;
        else
            return this.dodsname;
    }

    /**
     * Store the original dods name
     *
     * @param name The original name from the DDS/DAS
     */
    public void setDODSName(String name)
    {
        this.dodsname = name;
    }

    /**
     * Get the Full name of this object. Certain characters are
     * backslash escaped (see NetcdfFile)
     *
     * @return full name with backslash escapes
     */
    public String getFullName()
    {
        return NetcdfFile.makeFullName(this);
    /* getting called before complete
    if (this.fullName == null)
      this.fullName = NetcdfFile.makeFullName(this);
    return this.fullName; */
    }

    /**
     * Alias for getFullName
     *
     * @return full name with backslash escapes
     */
    public String getFullNameEscaped()
    {
        return getFullName();
    }

    /**
     * getName is deprecated because, as the code below shows,
     * it has no consistent meaning. Sometimes it returns
     * the short name, sometimes it returns the full name.
     *
     * @deprecated Replaced by {@link #getShortName()} and {@link #getFullName()}
     */
    @Deprecated
    public String getName()
    {
        switch (sort) {
        case ATTRIBUTE:
        case DIMENSION:
        case ENUMERATION:
            // for these cases, getName is getShortName
            return getShortName();

        case VARIABLE: // Atomic
        case SEQUENCE:
        case STRUCTURE:
        case GROUP:
            // for these cases, getName is getFullName
            return getFullName();
        default:
            break;
        }
        return getShortName(); // default
    }

    // experimental
    public abstract void hashCodeShow(Indent indent);

    // Override the node's hashCode for subclasses of CDMNode.

    public int localhash()
    {
        return super.hashCode();
    }

    /**
     * NetcdfDataset can end up wrapping a variable
     * in multiple wrapping classes (e.g. VariableDS).
     * Goal of this procedure is to get down to the
     * lowest level Variable instance
     *
     * @param node possibly wrapped ode
     * @return the lowest level node instance
     */
    static public CDMNode
    unwrap(CDMNode node)
    {
        if(!(node instanceof Variable))
            return node;
        Variable inner = (Variable) node;
        for(; ; ) {
            if(inner instanceof VariableDS) {
                VariableDS vds = (VariableDS) inner;
                inner = vds.getOriginalVariable();
                if(inner == null) {
                    inner = vds;
                    break;
                }
            } else if(inner instanceof StructureDS) {
                StructureDS sds = (StructureDS) inner;
                inner = sds.getOriginalVariable();
                if(inner == null) {
                    inner = sds;
                    break;
                }
            } else break; // base case we have straight Variable or Stucture
        }
        return inner;
    }

    public Object annotation(Object id)
    {
        return (this.annotations == null ? null : this.annotations.get(id));
    }

    public Object annotate(Object id, Object value)
    {
        if(annotations ==  null)
            annotations = new HashMap<>();
        Object old = annotations.get(id);
        annotations.put(id,value);
        return old;
    }

}


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

import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.*;

/** Convenience class for constructing popup menus.
 *
 * Example: add to a JTable:
 *
 *    JTable jtable = table.getJTable();
      PopupMenu csPopup = new PopupMenu(jtable, "Options");
      csPopup.addAction("Show Declaration", new AbstractAction() {
        public void actionPerformed(ActionEvent e) {
          showDeclaration(table);
        }
      });

 *
 */

public class PopupMenu extends JPopupMenu {
  private JComponent parent;

  /** Constructor.
    *
    * @param parent A MouseListener is added to this JComponent.
    * @param menuTitle title of the popup menu.
    */
   public PopupMenu(JComponent parent, String menuTitle) {
     this( parent, menuTitle, false);
   }

  /** Constructor.
    *
    * @param parent A MouseListener is added to this JComponent.
    * @param menuTitle title of the popup menu.
   *  @param anyButton if true, any button activates, otherwise use MouseEvent.isPopupTrigger
    */
   public PopupMenu(JComponent parent, String menuTitle, boolean anyButton) {
     super(menuTitle);
     this.parent = parent;
     setBorderPainted( true);
     setLabel( menuTitle);

     parent.addMouseListener( new PopupTriggerListener(anyButton) {
       public void showPopup(java.awt.event.MouseEvent e) {
         show(PopupMenu.this.parent, e.getX(), e.getY());
       }
     });
   }

   /** Add an action to the popup menu.
   * Note that the menuName is made the NAME value of the action.
   * @param menuName name of the action on the menu.
   * @param act the Action.
   */
  public void addAction( String menuName, Action act) {
    act.putValue( Action.NAME, menuName);
    super.add(act);
  }

  /** Add an action to the popup menu, with an icon.
   * Note that the menuName is made the NAME value of the action.
   * @param menuName name of the action on the menu.
   * @param act the Action.
   */
  public void addAction( String menuName, String iconName, Action act) {
    addAction( menuName, BAMutil.getIcon( iconName, true), act);
  }

  /** Add an action to the popup menu, with an icon.
   * Note that the menuName is made the NAME value of the action.
   * @param menuName name of the action on the menu.
   * @param act the Action.
   */
  public void addAction( String menuName, ImageIcon icon, Action act) {
    act.putValue( Action.NAME, menuName);
    act.putValue( Action.SMALL_ICON, icon);
    JMenuItem mi = add(act);
    mi.setHorizontalTextPosition( SwingConstants.LEFT );
  }

  /** Add an action to the popup menu, using a JCheckBoxMenuItem.
   * Fetch the toggle state using:
   * <pre>Boolean state = (Boolean) act.getValue(BAMutil.STATE); </pre>
   * @param menuName name of the action on the menu.
   * @param act the Action.
   * @param state : initial state of the checkbox
   */
  public void addActionCheckBox( String menuName, AbstractAction act, boolean state) {
    JMenuItem mi = new JCheckBoxMenuItem(menuName, state);
    mi.addActionListener( new BAMutil.ActionToggle(act, mi));
    act.putValue(BAMutil.STATE, new Boolean(state));
    add( mi);
  }

  public JComponent getParentComponent() { return parent; }

  public static abstract class PopupTriggerListener extends MouseAdapter {
    private boolean anyButton = false;
    PopupTriggerListener( boolean anyButton) {
      this.anyButton = anyButton;
    }

    PopupTriggerListener( ) {}

    public void mousePressed (MouseEvent e) {
      // System.out.println( "PopupTriggerListener "+e);
      if(anyButton || e.isPopupTrigger())
        showPopup(e);
    }
    public void mouseClicked (MouseEvent e) {
      if(anyButton || e.isPopupTrigger())
        showPopup(e);
    }
    public void mouseReleased (MouseEvent e) {
      if(anyButton || e.isPopupTrigger())
        showPopup(e);
    }

    public abstract void showPopup(MouseEvent e);
  }
}