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

import ucar.ma2.Array;
import ucar.ma2.DataType;
import ucar.ma2.InvalidRangeException;
import ucar.ma2.Section;
import ucar.nc2.*;
import ucar.nc2.constants.*;
import ucar.nc2.ft2.coverage.*;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.util.Optional;
import ucar.unidata.geoloc.*;
import ucar.unidata.geoloc.projection.LatLonProjection;

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

/**
 * Write CF Compliant Grid file from a Coverage.
 * First, single coverage only.
 * - The idea is to subset the coordsys, use that for the file's metadata.
 * - Then subset the grid, and write out the data. Check that the grid's metadata matches.
 *
 * @author caron
 * @since 5/8/2015
 */
public class CFGridCoverageWriter2 {
  static private final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(CFGridCoverageWriter2.class);
  static private final boolean show = false;

  static private final String BOUNDS = "_bounds";
  static private final String BOUNDS_DIM = "bounds_dim"; // dimension of length 2, can be used by any bounds coordinate

  /**
   * Write a netcdf/CF file from a CoverageDataset

   * @param gdsOrg            the CoverageDataset
   * @param gridNames         the list of coverage names to be written, or null for all
   * @param subset            defines the requested subset, or null to include everything in gdsOrg
   * @param tryToAddLatLon2D  add 2D lat/lon coordinates, if possible
   * @param testSizeOnly      dont write, just return expected size
   * @param writer            this does the actual writing, may be null if testSizeOnly=true
   * @return  the total number of bytes that the variables in the output file occupy. This is NOT the same as the
   *          size of the the whole output file, but it's close.
   * @throws IOException
   * @throws InvalidRangeException
   */
  public static ucar.nc2.util.Optional<Long> writeOrTestSize(CoverageCollection gdsOrg, List<String> gridNames,
          SubsetParams subset, boolean tryToAddLatLon2D, boolean testSizeOnly, NetcdfFileWriter writer)
          throws IOException, InvalidRangeException {
    CFGridCoverageWriter2 writer2 = new CFGridCoverageWriter2();
    return writer2.writeFile(gdsOrg, gridNames, subset, tryToAddLatLon2D, testSizeOnly, writer);
  }

  private ucar.nc2.util.Optional<Long> writeFile(CoverageCollection gdsOrg, List<String> gridNames,
          SubsetParams subsetParams, boolean tryToAddLatLon2D, boolean testSizeOnly, NetcdfFileWriter writer)
          throws IOException, InvalidRangeException {
    if (gridNames == null) {  // want all of them
      gridNames = new LinkedList<>();

      for (Coverage coverage : gdsOrg.getCoverages()) {
        gridNames.add(coverage.getName());
      }
    }

    if (subsetParams == null) {
      subsetParams = new SubsetParams();
    }

    if (writer == null) {
      if (testSizeOnly) {
        writer = NetcdfFileWriter.createNew(null, false);  // null location. It's ok; we'll never write the file.
      } else {
        throw new NullPointerException("writer must be non-null when testSizeOnly == false.");
      }
    }

    // We need global attributes, subsetted axes, transforms, and the coverages with attributes and referencing
    // subsetted axes.
    Optional<CoverageCollection> opt = CoverageSubsetter2.makeCoverageDatasetSubset(gdsOrg, gridNames, subsetParams);
    if (!opt.isPresent())
      return ucar.nc2.util.Optional.empty(opt.getErrorMessage());

    CoverageCollection subsetDataset = opt.get();

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

    addGlobalAttributes(subsetDataset, writer);
    addDimensions(subsetDataset, writer);
    addCoordinateAxes(subsetDataset, writer);
    addCoverages(subsetDataset, writer);
    addCoordTransforms(subsetDataset, writer);

    boolean shouldAddLatLon2D = shouldAddLatLon2D(tryToAddLatLon2D, subsetDataset);
    if (shouldAddLatLon2D) {
      addLatLon2D(subsetDataset, writer);
    }

    addCFAnnotations(subsetDataset, writer, shouldAddLatLon2D);

    long totalSizeOfVars = 0;
    // This is a hack to get the root group of writer's underlying NetcdfFile. See the method's Javadoc.
    Group rootGroup = writer.addGroup(null, null);

    // In this class, we've only added vars to the root group, so this is all we need to worry about for size calc.
    for (Variable var : rootGroup.getVariables()) {
      totalSizeOfVars += var.getSize() * var.getElementSize();
    }

    if (!testSizeOnly) {
      // Actually create file and write variable data to it.
      writer.setLargeFile(isLargeFile(totalSizeOfVars));
      writer.create();

      writeCoordinateData(subsetDataset, writer);
      writeCoverageData(gdsOrg, subsetParams, subsetDataset, writer);

      if (shouldAddLatLon2D) {
        writeLatLon2D(subsetDataset, writer);
      }

      writer.close();
    }

    return Optional.of(totalSizeOfVars);
  }

  /**
   * Returns {@code true} if we should add 2D latitude & longitude variables to the output file.
   * This method could return {@code false} for several reasons:
   *
   *

   *
{@code !tryToAddLatLon2D}

   *
{@code subsetDataset.getHorizCoordSys().isLatLon2D()}

   *
{@code !subsetDataset.getHorizCoordSys().isProjection()}

   *
{@code subsetDataset.getHorizCoordSys() instanceof LatLonProjection}

   *

   *
   * @param tryToAddLatLon2D  attempt to add 2D lat/lon vars to output file.
   * @param subsetDataset     subsetted version of the original CoverageCollection.
   * @return  {@code true} if we should add 2D latitude & longitude variables to the output file.
   */
  private boolean shouldAddLatLon2D(boolean tryToAddLatLon2D, CoverageCollection subsetDataset) {
    if (!tryToAddLatLon2D) {  // We don't even want 2D lat/lon vars.
      return false;
    }

    HorizCoordSys horizCoordSys = subsetDataset.getHorizCoordSys();
    if (horizCoordSys.isLatLon2D()) {  // We already have 2D lat/lon vars.
      return false;
    }
    if (!horizCoordSys.isProjection()) {  // CRS doesn't contain a projection, meaning we can't calc 2D lat/lon vars.
      return false;
    }

    Projection proj = horizCoordSys.getTransform().getProjection();
    if (proj instanceof LatLonProjection) {  // Projection is a "fake"; we already have lat/lon.
      return false;
    }

    return true;
  }

  private boolean isLargeFile(long total_size) {
    boolean isLargeFile = false;
    long maxSize = Integer.MAX_VALUE;
    if (total_size > maxSize) {
      logger.debug("Request size = {} Mbytes", total_size / 1000 / 1000);
      isLargeFile = true;
    }
    return isLargeFile;
  }

  private void addGlobalAttributes(CoverageCollection gds, NetcdfFileWriter writer) {
    // global attributes
    for (Attribute att : gds.getGlobalAttributes()) {
      if (att.getShortName().equals(CDM.FILE_FORMAT)) continue;
      if (att.getShortName().equals(_Coordinate._CoordSysBuilder)) continue;
      writer.addGroupAttribute(null, att);
    }

    Attribute att = gds.findAttributeIgnoreCase(CDM.CONVENTIONS);
    if (att == null || !att.getStringValue().startsWith("CF-"))  // preserve prev version of CF Convention if exists
      writer.addGroupAttribute(null, new Attribute(CDM.CONVENTIONS, "CF-1.0"));

    writer.addGroupAttribute(null, new Attribute("History",
            "Translated to CF-1.0 Conventions by Netcdf-Java CDM (CFGridCoverageWriter2)\n" +
                    "Original Dataset = " + gds.getName() + "; Translation Date = " + CalendarDate.present()));

    LatLonRect llbb = gds.getLatlonBoundingBox();
    if (llbb != null) {
      // this will replace any existing
      writer.addGroupAttribute(null, new Attribute(ACDD.LAT_MIN, llbb.getLatMin()));
      writer.addGroupAttribute(null, new Attribute(ACDD.LAT_MAX, llbb.getLatMax()));
      writer.addGroupAttribute(null, new Attribute(ACDD.LON_MIN, llbb.getLonMin()));
      writer.addGroupAttribute(null, new Attribute(ACDD.LON_MAX, llbb.getLonMax()));
    }
  }

  private void addDimensions(CoverageCollection subsetDataset, NetcdfFileWriter writer) {
    // each independent coordinate is a dimension
    Map<String, Dimension> dimHash = new HashMap<>();
    for (CoverageCoordAxis axis : subsetDataset.getCoordAxes()) {
      if (axis.getDependenceType() == CoverageCoordAxis.DependenceType.independent) {
        Dimension d = writer.addDimension(null, axis.getName(), axis.getNcoords());
        dimHash.put(axis.getName(), d);
      }

      if (axis.isInterval()) {
        if (null == dimHash.get(BOUNDS_DIM)) {
          Dimension d = writer.addDimension(null, BOUNDS_DIM, 2);
          dimHash.put(BOUNDS_DIM, d);
        }
      }
    }
  }

  private void addCoordinateAxes(CoverageCollection subsetDataset, NetcdfFileWriter writer) {
    for (CoverageCoordAxis axis : subsetDataset.getCoordAxes()) {
      String dims;

      if (axis.getDependenceType() == CoverageCoordAxis.DependenceType.independent) {
        dims = axis.getName();

      } else if (axis.getDependenceType() == CoverageCoordAxis.DependenceType.scalar) {
        dims = "";

      } else {
        dims = axis.getDependsOn();
      }

      boolean hasBounds = false;
      if (axis.isInterval()) {
        Variable vb = writer.addVariable(null, axis.getName()+BOUNDS, axis.getDataType(), dims+" "+BOUNDS_DIM);
        vb.addAttribute(new Attribute(CDM.UNITS, axis.getUnits()));
        hasBounds = true;
      }

      Variable v = writer.addVariable(null, axis.getName(), axis.getDataType(), dims);
      addVariableAttributes(v, axis.getAttributes());
      v.addAttribute(new Attribute(CDM.UNITS, axis.getUnits())); // override what was in att list
      if (hasBounds)
        v.addAttribute(new Attribute(CF.BOUNDS, axis.getName()+BOUNDS));
      if (axis.getAxisType() == AxisType.TimeOffset)
        v.addAttribute(new Attribute(CF.STANDARD_NAME, CF.TIME_OFFSET));
    }
  }

  private void addCoverages(CoverageCollection subsetDataset, NetcdfFileWriter writer) {
    for (Coverage grid : subsetDataset.getCoverages()) {
      Variable v = writer.addVariable(null, grid.getName(), grid.getDataType(), grid.getIndependentAxisNamesOrdered());
      addVariableAttributes(v, grid.getAttributes());
    }
  }

  private void addVariableAttributes(Variable v, List<Attribute> atts) {
    for (Attribute att : atts) {
      if (att.getShortName().startsWith("_Coordinate")) continue;
      if (att.getShortName().startsWith("_Chunk")) continue;
      v.addAttribute(att);
    }
  }

  private void addCoordTransforms(CoverageCollection subsetDataset, NetcdfFileWriter writer) {
    for (CoverageTransform ct : subsetDataset.getCoordTransforms()) {
      // scalar coordinate transform variable - container for transform info
      Variable ctv = writer.addVariable(null, ct.getName(), DataType.INT, "");

      for (Attribute att : ct.getAttributes())
        ctv.addAttribute(att);
    }
  }

  private void addLatLon2D(CoverageCollection subsetDataset, NetcdfFileWriter writer) {
    HorizCoordSys horizCoordSys = subsetDataset.getHorizCoordSys();
    CoverageCoordAxis1D xAxis = horizCoordSys.getXAxis();
    CoverageCoordAxis1D yAxis = horizCoordSys.getYAxis();

    Dimension xDim = writer.findDimension(xAxis.getName());
    Dimension yDim = writer.findDimension(yAxis.getName());
    assert xDim != null : "We should've added X dimension in addDimensions().";
    assert yDim != null : "We should've added Y dimension in addDimensions().";

    List<Dimension> dims = Arrays.asList(yDim, xDim);

    Variable latVar = writer.addVariable("lat", DataType.DOUBLE, dims);
    latVar.addAttribute(new Attribute(CDM.UNITS, CDM.LAT_UNITS));
    latVar.addAttribute(new Attribute(CF.STANDARD_NAME, CF.LATITUDE));
    latVar.addAttribute(new Attribute(CDM.LONG_NAME, "latitude coordinate"));
    latVar.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Lat.toString()));

    Variable lonVar = writer.addVariable("lon", DataType.DOUBLE, dims);
    lonVar.addAttribute(new Attribute(CDM.UNITS, CDM.LON_UNITS));
    lonVar.addAttribute(new Attribute(CF.STANDARD_NAME, CF.LONGITUDE));
    lonVar.addAttribute(new Attribute(CDM.LONG_NAME, "longitude coordinate"));
    lonVar.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Lon.toString()));
  }

  private void addCFAnnotations(CoverageCollection gds, NetcdfFileWriter writer, boolean shouldAddLatLon2D) {
    for (Coverage grid : gds.getCoverages()) {
      CoverageCoordSys gcs = grid.getCoordSys();

      Variable newV = writer.findVariable(grid.getName());
      if (newV == null) {
        logger.error("CFGridCoverageWriter2 cant find " + grid.getName() + " in writer ");
        continue;
      }

      // annotate Variable for CF
      Formatter coordsAttribValFormatter = new Formatter();
      for (String axisName : grid.getCoordSys().getAxisNames()) {
        coordsAttribValFormatter.format("%s ", axisName);
      }

      if (shouldAddLatLon2D) {
        assert writer.findVariable("lat") != null : "We should've added lat variable in addLatLon2D()";
        assert writer.findVariable("lon") != null : "We should've added lon variable in addLatLon2D()";
        coordsAttribValFormatter.format("lat lon");
      }

      newV.addAttribute(new Attribute(CF.COORDINATES, coordsAttribValFormatter.toString()));

      // add reference to coordinate transform variables
      CoverageTransform ct = gcs.getHorizTransform();
      if (ct != null && ct.isHoriz())
        newV.addAttribute(new Attribute(CF.GRID_MAPPING, ct.getName()));

      // LOOK what about vertical ?
    }

    for (CoverageCoordAxis axis : gds.getCoordAxes()) {
      Variable newV = writer.findVariable(axis.getName());
      if (newV == null) {
        logger.error("CFGridCoverageWriter2 cant find " + axis.getName() + " in writer ");
        continue;
      }

      // LOOK: Commented out because CoverageCoordAxis doesn't have any info about "positive" wrt vertical axes.
      // To fix, we'd need to add that metadata when building the CRS.
      /* if ((axis.getAxisType() == AxisType.Height) || (axis.getAxisType() == AxisType.Pressure) ||
            (axis.getAxisType() == AxisType.GeoZ)) {
        if (null != axis.getPositive())
          newV.addAttribute(new Attribute(CF.POSITIVE, axis.getPositive()));
      } */

      if (axis.getAxisType() == AxisType.Lat) {
        newV.addAttribute(new Attribute(CDM.UNITS, CDM.LAT_UNITS));
        newV.addAttribute(new Attribute(CF.STANDARD_NAME, CF.LATITUDE));
      }
      if (axis.getAxisType() == AxisType.Lon) {
        newV.addAttribute(new Attribute(CDM.UNITS, CDM.LON_UNITS));
        newV.addAttribute(new Attribute(CF.STANDARD_NAME, CF.LONGITUDE));
      }
      if (axis.getAxisType() == AxisType.GeoX) {
        newV.addAttribute(new Attribute(CF.STANDARD_NAME, CF.PROJECTION_X_COORDINATE));
      }
      if (axis.getAxisType() == AxisType.GeoY) {
        newV.addAttribute(new Attribute(CF.STANDARD_NAME, CF.PROJECTION_Y_COORDINATE));
      }
      if (axis.getAxisType() == AxisType.Ensemble) {
        newV.addAttribute(new Attribute(CF.STANDARD_NAME, CF.ENSEMBLE));
      }
    }
  }

  private void writeCoordinateData(CoverageCollection subsetDataset, NetcdfFileWriter writer)
          throws IOException, InvalidRangeException {
    for (CoverageCoordAxis axis : subsetDataset.getCoordAxes()) {
      Variable v = writer.findVariable(axis.getName());
      if (v != null) {
        if (show) System.out.printf("CFGridCoverageWriter2 write axis %s%n", v.getNameAndDimensions());
        writer.write(v, axis.getCoordsAsArray());
      } else {
        logger.error("CFGridCoverageWriter2 No variable for %s%n", axis.getName());
      }

      if (axis.isInterval()) {
        Variable vb = writer.findVariable(axis.getName() + BOUNDS);
        writer.write(vb, axis.getCoordBoundsAsArray());
      }
    }
  }

  private void writeCoverageData(CoverageCollection gdsOrg, SubsetParams subsetParams,
          CoverageCollection subsetDataset, NetcdfFileWriter writer) throws IOException, InvalidRangeException {
    for (Coverage coverage : subsetDataset.getCoverages()) {
      // we need to call readData on the original
      Coverage coverageOrg = gdsOrg.findCoverage(coverage.getName());
      GeoReferencedArray array = coverageOrg.readData(subsetParams);

      // test conform to whatever axis.getCoordsAsArray() returns
      checkConformance(coverage, array, gdsOrg.getName());

      Variable v = writer.findVariable(coverage.getName());
      if (show) System.out.printf("CFGridCoverageWriter2 write coverage %s%n", v.getNameAndDimensions());
      writer.write(v, array.getData());
    }
  }

  private void writeLatLon2D(CoverageCollection subsetDataset, NetcdfFileWriter writer)
          throws IOException, InvalidRangeException {
    HorizCoordSys horizCoordSys = subsetDataset.getHorizCoordSys();
    CoverageCoordAxis1D xAxis = horizCoordSys.getXAxis();
    CoverageCoordAxis1D yAxis = horizCoordSys.getYAxis();

    Projection proj = horizCoordSys.getTransform().getProjection();
    ProjectionPointImpl projPoint = new ProjectionPointImpl();
    LatLonPointImpl latlonPoint = new LatLonPointImpl();

    double[] xData = (double[]) xAxis.getCoordsAsArray().get1DJavaArray(DataType.DOUBLE);
    double[] yData = (double[]) yAxis.getCoordsAsArray().get1DJavaArray(DataType.DOUBLE);

    int numX = xData.length;
    int numY = yData.length;

    double[] latData = new double[numX * numY];
    double[] lonData = new double[numX * numY];

    // create the data
    for (int i = 0; i < numY; i++) {
      for (int j = 0; j < numX; j++) {
        projPoint.setLocation(xData[j], yData[i]);
        proj.projToLatLon(projPoint, latlonPoint);
        latData[i * numX + j] = latlonPoint.getLatitude();
        lonData[i * numX + j] = latlonPoint.getLongitude();
      }
    }

    Variable latVar = writer.findVariable("lat");
    assert latVar != null : "We should have added lat var in addLatLon2D().";
    Array latDataArray = Array.factory(DataType.DOUBLE, new int[] { numY, numX }, latData);
    writer.write(latVar, latDataArray);

    Variable lonVar = writer.findVariable("lon");
    assert lonVar != null : "We should have added lon var in addLatLon2D().";
    Array lonDataArray = Array.factory(DataType.DOUBLE, new int[] { numY, numX }, lonData);
    writer.write(lonVar, lonDataArray);
  }

  private void checkConformance(Coverage gridSubset, GeoReferencedArray geo, String where) {
    CoverageCoordSys csys = gridSubset.getCoordSys();

    CoverageCoordSys csysData = geo.getCoordSysForData();

    //System.out.printf("    csys=%s%n", csys);
    //System.out.printf("csysData=%s%n", csysData);

    Section s = new Section(csys.getShape());
    Section so = new Section(csysData.getShape());

    boolean ok = s.conformal(so);

    int[] dataShape = geo.getData().getShape();
    //System.out.printf("dataShape=%s%n", Misc.showInts(dataShape));
    Section sdata = new Section(dataShape);
    boolean ok2 = s.conformal(sdata);

    if (!ok || !ok2)
      logger.warn("CFGridCoverageWriter2 checkConformance fails " +where);
  }
}


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

package ucar.nc2.grib.grib1;

import ucar.nc2.grib.GribNumbers;
import ucar.nc2.grib.GribStatType;
import ucar.nc2.grib.grib1.tables.Grib1Customizer;

import javax.annotation.concurrent.Immutable;

/**
 * Time coordinate from the PDS.
 * Process information from GRIB-1 Table 4: "Forecast time unit"
 * Process information from GRIB-1 Table 5: "Time range indicator"
 *
 * Handles standard (WMO), Grib1Customizer must override / augment
 *
 * @author John
 * @since 9/4/11
 */
@Immutable
public class Grib1ParamTime {

    // code table 5 - 2010 edition of WMO manual on codes
  public static String getTimeTypeName(int timeRangeIndicator) {
    String timeRange;

    switch (timeRangeIndicator) {

      /* Forecast product valid for reference time + P1 (P1 > 0), or
        Uninitialized analysis product for reference time (P1 = 0), or
        Image product for reference time (P1 = 0) */
      case 0:
        timeRange = "Uninitialized analysis / image product / forecast product valid for RT + P1";
        break;

      // Initialized analysis product for reference time (P1 = 0)
      case 1:
        timeRange = "Initialized analysis product for reference time";
        break;

      // Product with a valid time ranging between reference time + P1 and reference time + P2
      case 2:
        timeRange = "product valid, interval = (RT + P1) to (RT + P2)";
        break;

      // Average (reference time + P1 to reference time + P2)
      case 3:
        timeRange = "Average, interval = (RT + P1) to (RT + P2)";
        break;

      /* Accumulation  (reference  time  +  P1  to  reference  time  +  P2)  product  considered  valid  at
        reference time + P2 */
      case 4:
        timeRange = "Accumulation, interval = (RT + P1) to (RT + P2)";
        break;

      /* Difference  (reference  time  +  P2  minus  reference  time  +  P1)  product  considered  valid  at
        reference time + P2 */
      case 5:
        timeRange = "Difference, interval = (RT + P2) - (RT + P1)";
        break;

      // Average (reference time - P1 to reference time - P2)
      case 6:
        timeRange = "Average, interval = (RT - P1) to (RT - P2)";
        break;

      // Average (reference time - P1 to reference time + P2)
      case 7:
        timeRange = "Average, interval = (RT - P1) to (RT + P2)";
        break;

      // P1 occupies octets 19 and 20; product valid at reference time + P1
      case 10:
        timeRange = "product valid at RT + P1";
        break;

      /* Climatological  mean  value:  multiple  year  averages  of  quantities  which  are  themselves
        means over some period of time (P2) less than a year. The reference time (R) indicates the
        date and time of the start of a period of time, given by R to R + P2, over which a mean is
        formed; N indicates the number of such period-means that are averaged together to form
        the  climatological  value,  assuming  that  the  N  period-mean  fields  are  separated  by  one
        year. The reference time indicates the start of the N-year climatology.

        If P1 = 0 then the data averaged in the basic interval P2 are assumed to be continuous, i.e. all available data
        are simply averaged together.

        If P1 = 1 (the unit of time  octet 18, Code table 4  is not
        relevant here) then the data averaged together in the basic interval P2 are valid only at the
        time (hour, minute) given in the reference time, for all the days included in the P2 period.
        The units of P2 are given by the contents of octet 18 and Code table 4 */
      case 51:
        timeRange = "Climatological mean values from RT to (RT + P2)";
        // if (p1 == 0) timeRange += " continuous";
        break;

      /* Average  of  N  forecasts  (or  initialized  analyses);  each  product  has  forecast  period  of  P1
        (P1 = 0 for initialized analyses); products have reference times at intervals of P2, beginning
        at the given reference time */
      case 113:
        timeRange = "Average of N forecasts, intervals = (refTime + i * P2, refTime + i * P2 + P1)";
        break;

      /* Accumulation of N forecasts (or initialized analyses); each product has forecast period of
        P1  (P1  =  0  for  initialized  analyses);  products  have  reference  times  at  intervals  of  P2,
        beginning at the given reference time */
      case 114:
        timeRange = "Accumulation of N forecasts, intervals = (refTime + i * P2, refTime + i * P2 + P1)";
        break;

      /* Average of N forecasts, all with the same reference time; the first has a forecast period of
         P1, the remaining forecasts follow at intervals of P2 */
      case 115:
        timeRange = "Average of N forecasts, intervals = (refTime, refTime + P1 + i * P2)";
        break;

      /* Accumulation  of  N  forecasts,  all  with  the  same  reference  time;  the  first  has  a  forecast
        period of P1, the remaining forecasts follow at intervals of P2 */
      case 116:
        timeRange = "Accumulation of N forecasts, intervals = (refTime, refTime + P1 + i * P2)";
        break;

      /* Average of N forecasts; the first has a forecast period of P1, the subsequent ones have
        forecast periods reduced from the previous one by an interval of P2; the reference time for
        the first is given in octets 13 to 17, the subsequent ones have reference times increased
        from the previous one by an interval of P2. Thus all the forecasts have the same valid time,
        given by the initial reference time + P1 */
      case 117:
        timeRange = "Average of N forecasts, intervals = (refTime + i * P2, refTime + P1)";
        break;

      /* Temporal  variance,  or  covariance,  of  N  initialized  analyses;  each  product  has  forecast
        period of P1 = 0; products have reference times at intervals of P2, beginning at the given
        reference time */
      case 118:
        timeRange = "Temporal variance or covariance of N initialized analyses, timeCoord = (refTime + i * P2)";
        break;

      /* Standard deviation of N forecasts, all with the same reference time with respect to the time
        average of forecasts; the first forecast has a forecast period of P1, the remaining forecasts
        follow at intervals of P2 */
      case 119:
        timeRange = "Standard Deviation of N forecasts, timeCoord = (refTime + P1 + i * P2)";
        break;

      // ECMWF "Average of N Forecast" added 11/21/2014. pretend its WMO standard. maybe should move to ecmwf ??
      // see "http://emoslib.sourcearchive.com/documentation/000370.dfsg.2/grchk1_8F-source.html"
      // C     Add Time range indicator = 120 Average of N Forecast. Each product
      // C             is an accumulation from forecast lenght P1 to forecast
      // C              lenght P2, with reference times at intervals P2-P1
      case 120:
        timeRange = "Average of N Forecasts (ECMWF), accumulation from forecast P1 to P2, with reference times at intervals P2-P1";
        break;

      // Average of N uninitialized analyses, starting at the reference time, at intervals of P2
      case 123:
        timeRange = "Average of N uninitialized analyses, intervals = (refTime, refTime + i * P2)";
        break;

      // Accumulation of N uninitialized analyses, starting at the reference time, at intervals of P2
      case 124:
        timeRange = "Accumulation of N uninitialized analyses, intervals = (refTime, refTime + i * P2)";
        break;

      /* Standard deviation of N forecasts, all with the same reference time with respect to time
        average of the time tendency of forecasts; the first forecast has a forecast period of P1,
        the remaining forecasts follow at intervals of P2 */
      case 125:
        timeRange = "Standard deviation of N forecasts, intervals = (refTime, refTime + P1 + i * P2)";
        break;

      default:
        timeRange = "Unknown Time Range Indicator " + timeRangeIndicator;
    }

    return timeRange;
  }

  ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  private final Grib1Customizer cust;   // passed in

  private final int timeRangeIndicator; // code Table 5 (octet 21)
  private final boolean isInterval;
  private final int start, end;     // for intervals
  private final int forecastTime;   // for non-intervals

  public Grib1ParamTime(Grib1Customizer cust, int timeRangeIndicator, boolean isInterval, int start, int end, int forecastTime) {
    this.cust = cust;
    this.timeRangeIndicator = timeRangeIndicator;
    this.isInterval = isInterval;
    this.start = start;
    this.end = end;
    this.forecastTime = forecastTime;
  }

  /**
   * Handles GRIB-1 code table 5 : "Time range indicator".
   *
   * @param cust customizer
   * @param pds the Grib1SectionProductDefinition
   */
  public Grib1ParamTime(Grib1Customizer cust, Grib1SectionProductDefinition pds) {
    this.cust = cust;

    int p1 = pds.getTimeValue1();  // octet 19
    int p2 = pds.getTimeValue2();  // octet 20
    int timeRangeIndicatorLocal = pds.getTimeRangeIndicator(); // octet 21
    int n = pds.getNincluded();

    int startLocal = 0;
    int endLocal = 0;
    int forecastTimeLocal = 0;
    boolean isIntervalLocal = false;

    switch (timeRangeIndicatorLocal) {

      /* Forecast product valid for reference time + P1 (P1 > 0), or
        Uninitialized analysis product for reference time (P1 = 0), or
        Image product for reference time (P1 = 0) */
      case 0:
        forecastTimeLocal = p1;
        break;

      // Initialized analysis product for reference time (P1 = 0)
      case 1:
        // accept defaults
        break;

      case 2:  // Product with a valid time ranging between reference time + P1 and reference time + P2
      case 3:  // Average (reference time + P1 to reference time + P2)
      case 4:  // Accumulation  (reference  time  +  P1  to  reference  time  +  P2)  product  considered  valid  at reference time + P2
      case 5:  // Difference  (reference  time  +  P2  minus  reference  time  +  P1)  product  considered  valid  at reference time + P2
        startLocal = p1;
        endLocal = p2;
        isIntervalLocal = true;
        break;

      // Average (reference time - P1 to reference time - P2)
      case 6:
        startLocal = -p1;
        endLocal = -p2;
        isIntervalLocal = true;
        break;

      // Average (reference time - P1 to reference time + P2)
      case 7:
        startLocal = -p1;
        endLocal = p2;
        isIntervalLocal = true;
        break;

      // P1 occupies octets 19 and 20; product valid at reference time + P1
      case 10:
        forecastTimeLocal = GribNumbers.int2(p1, p2);
        break;

      /* Climatological  mean  value:  multiple  year  averages  of  quantities  which  are  themselves
        means over some period of time (P2) less than a year. The reference time (R) indicates the
        date and time of the start of a period of time, given by R to R + P2, over which a mean is
        formed; N indicates the number of such period-means that are averaged together to form
        the  climatological  value,  assuming  that  the  N  period-mean  fields  are  separated  by  one
        year. The reference time indicates the start of the N-year climatology. If P1 = 0 then the
        data averaged in the basic interval P2 are assumed to be continuous, i.e. all available data
        are simply averaged together. If P1 = 1 (the unit of time – octet 18, Code table 4 – is not
        relevant here) then the data averaged together in the basic interval P2 are valid only at the
        time (hour, minute) given in the reference time, for all the days included in the P2 period.
        The units of P2 are given by the contents of octet 18 and Code table 4 */
      case 51:  // LOOK ??
        forecastTimeLocal = p2;
        break;

      /* 113: Average  of  N  forecasts  (or  initialized  analyses);  each  product  has  forecast  period  of  P1
        (P1 = 0 for initialized analyses); products have reference times at intervals of P2, beginning
        at the given reference time.

         114: Accumulation of N forecasts (or initialized analyses); each product has forecast period of
                 P1  (P1  =  0  for  initialized  analyses);  products  have  reference  times  at  intervals  of  P2,
                 beginning at the given reference time
         115: Average of N forecasts, all with the same reference time; the first has a forecast period of
                  P1, the remaining forecasts follow at intervals of P2
         116: Accumulation  of  N  forecasts,  all  with  the  same  reference  time;  the  first  has  a  forecast
                 period of P1, the remaining forecasts follow at intervals of P2

         */
      case 113:
      case 114:
      case 115:
      case 116:
        forecastTimeLocal = p1;
        startLocal = p1;
        endLocal = (n > 0) ? p1 + (n-1) * p2 : p1;  // LOOK switch to n-1 on 3/13/2015
        isIntervalLocal = (n > 0);
        break;

      /* Average of N forecasts; the first has a forecast period of P1, the subsequent ones have
        forecast periods reduced from the previous one by an interval of P2; the reference time for
        the first is given in octets 13 to 17, the subsequent ones have reference times increased
        from the previous one by an interval of P2. Thus all the forecasts have the same valid time,
        given by the initial reference time + P1 */
      case 117:
        endLocal = p1;
        isIntervalLocal = true;
        break;

      /* Temporal  variance,  or  covariance,  of  N  initialized  analyses;  each  product  has  forecast
        period of P1 = 0; products have reference times at intervals of P2, beginning at the given
        reference time */
      case 118:
        endLocal = n * p2;
        isIntervalLocal = true;
        break;

      /* Standard deviation of N forecasts, all with the same reference time with respect to the time
        average of forecasts; the first forecast has a forecast period of P1, the remaining forecasts
        follow at intervals of P2 */
      case 119:
        startLocal = p1;
        endLocal = p1 + n * p2;
        isIntervalLocal = true;
        break;

      // ECMWF "Average of N Forecast" added 11/21/2014
      // see "http://emoslib.sourcearchive.com/documentation/000370.dfsg.2/grchk1_8F-source.html"
      // C     Add Time range indicator = 120 Average of N Forecast. Each product
      // C             is an accumulation from forecast lenght P1 to forecast
      // C              lenght P2, with reference times at intervals P2-P1
      case 120:
        startLocal = p1;
        endLocal = p2;
        isIntervalLocal = true;
        break;

       // Average of N uninitialized analyses, starting at the reference time, at intervals of P2
      case 123:
        endLocal = n * p2;
        isIntervalLocal = true;
        break;

      // Accumulation of N uninitialized analyses, starting at the reference time, at intervals of P2
      case 124:
        endLocal = n * p2;
        isIntervalLocal = true;
        break;

      /* Standard deviation of N forecasts, all with the same reference time with respect to time
        average of the time tendency of forecasts; the first forecast has a forecast period of P1,
        the remaining forecasts follow at intervals of P2 */
      case 125:
        endLocal = p1 + n * p2;
        isIntervalLocal = true;
        break;

      default:
        throw new IllegalArgumentException("PDS: Unknown Time Range Indicator " + timeRangeIndicatorLocal);
    }

    // added 11/30/2014. If interval (0,0), change to non interval at 0
    // analysis (0-hour) datasets use these (0,0) intervals, they are initialization values (I think).
    // by eliminating the extra coordinate, things get simpler.
    /* LOOK default we eliminate (0,0) so maybe not needed?  3/13/2015
    if (isIntervalLocal && (p1 == p2) && (p1 == 0)) {
      timeRangeIndicatorLocal = 1;
      forecastTimeLocal = 0;
      startLocal = endLocal = 0;
      isIntervalLocal = false;
    } */

    // rigamorole to keep things final
    timeRangeIndicator = timeRangeIndicatorLocal;
    isInterval = isIntervalLocal;
    start = startLocal;
    end = endLocal;
    forecastTime = forecastTimeLocal;
  }

  /**
   * Get interval [start, end] since reference time in units of timeUnit, must be an interval.
   * @return interval [start, end]
   */
  public int[] getInterval() {
    return new int[]{start, end};
  }

  /**
   * Get interval size (end - start) in units of timeUnit, only if an interval.
   * @return interval size
   */
  public int getIntervalSize() {
    return isInterval ? end - start : 0;
  }

  /**
   * Is this an interval time coordinate
   * @return If an interval time coordinate
   */
  public boolean isInterval() {
    return isInterval;
  }

  /**
   * Forecast time since reference time in units of timeUnit, only if not an interval.
   * @return Forecast time
   */
  public int getForecastTime() {
    return forecastTime;
  }

  /**
   * The time unit name (code table 5)
   * @return time unit name
   */
  public String getTimeTypeName() {
    return cust.getTimeTypeName(timeRangeIndicator);
  }

   /**
   * The time unit statistical type, derived from code table 5)
   * @return time unit statistical type
   */
  public GribStatType getStatType() {
    return cust.getStatType(timeRangeIndicator);
  }

  /**
   * A string representation of the time coordinate, whether its an interval or not.
   * @return string representation of the time coordinate
   */
  public String getTimeCoord() {
    if (isInterval()) {
      int[] intv = getInterval();
      return intv[0] + "-" + intv[1];
    }
    return Integer.toString(getForecastTime());
  }
}


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

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

package thredds.catalog.query;

import java.util.*;

/**
 * Implementation of a thredds DQC object.
 *
 * @author john caron
 */

public class QueryCapability {

  private String createFrom, name, version;
  private Query query;
  private ArrayList selectors = new ArrayList(); // Selector
  private ArrayList uniqueSelectors = new ArrayList(); // no duplicate ids
  private ArrayList userInterfaces = new ArrayList();
  private Selector ss = null;

  private boolean fatalError = false;
  private StringBuffer errLog = new StringBuffer();

  // package private
  QueryCapability() { }

   /**
    * Construct from fields in XML catalog.
    * @param urlString the DQC document URI
    * @param name name of DQC document
    * @param version version string
    */
  public QueryCapability( String urlString, String name, String version) {
    this.createFrom = urlString;
    this.name = name;
    this.version = version;
  }

  /**
   * Append an error message to the message log. Call check() to get the log when
   *  everything is done.
   * @param message append this message to log
   * @param fatal true if this is a fatal error.
   */
  public void appendErrorMessage( String message, boolean fatal) {
    errLog.append( message);
    errLog.append( "\n");
    fatalError = fatalError || fatal;
  }

  public String getErrorMessages() { return errLog.toString(); }
  public boolean hasFatalError() { return fatalError; }

  public void addSelector( Selector s) { selectors.add(s); }
  public void setQuery( Query q) { this.query = q; }

  public String getName() { return name; }
  public String getVersion() { return version; }
  public Query getQuery() { return query; }
  public ArrayList getSelectors() { return selectors; }
  public ArrayList getAllUniqueSelectors() { return uniqueSelectors; }

  public String getCreateFrom() { return( this.createFrom ); }

  /** Get ServiceSelector */
  public Selector getServiceSelector() { return ss; }
  /** Set ServiceSelector */
  public void setServiceSelector( Selector ss) { this.ss = ss; }

  public void addUserInterface( Object s) { userInterfaces.add(s); }
  public ArrayList getUserInterfaces( ) { return userInterfaces; }

     /** String representation */
  public String toString() {
    return "name:<"+name+">";
  }

  public void addUniqueSelector( Selector s) {
    if (!uniqueSelectors.contains(s))
      uniqueSelectors.add(s);
    selectors.add( s);
  }

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

  /* boolean validate(StringBuffer out, boolean show) {
    boolean isValid = true;
    return isValid;
  } */

   public boolean equals(Object o) {
     if (this == o) return true;
     if (!(o instanceof QueryCapability)) return false;
     return o.hashCode() == this.hashCode();
  }
  /** Override Object.hashCode() to implement equals. */
  public int hashCode() {
    if (hashCode == 0) {
      int result = 17;
      result = 37*result + getName().hashCode();
      result = 37*result + getQuery().hashCode();
      result = 37*result + getSelectors().hashCode();

      hashCode = result;
    }
    return hashCode;
  }
  private volatile int hashCode = 0; // Bloch, item 8

}


/* Copyright 2016, University Corporation for Atmospheric Research
   See the LICENSE.txt file for more information.
*/

package thredds.server.reify;

import ucar.httpservices.HTTPUtil;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;

import static thredds.server.reify.LoadCommon.Command;

/**
 * Process an HttpRequest to extract common reification parameters
 */
class Parameters
{
    //////////////////////////////////////////////////
    // Constants

    //////////////////////////////////////////////////
    // Constructor arguments

    public HttpServletRequest req;

    // computed

    Map<String, String[]> params;

    public Map<String, String> testinfo = new HashMap<>();

    // Public
    public Command command = null;

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

    public Parameters(HttpServletRequest req)
            throws IOException
    {
        assert req != null;

        this.req = req;
        this.params = new HashMap<String, String[]>();
        if(req.getParameterMap() == null) {
            this.command = Command.NONE;
        } else {
            this.params.putAll(req.getParameterMap());
            String s = getparam("testinfo");
            if(s != null)
                this.testinfo = LoadCommon.parseMap(s, ';', true);
            // Command
            s = getparam("request");
            if(s == null)
                this.command = Command.NONE;
            else {
                this.command = Command.parse(s);
                if(this.command == null)
                    throw new IOException("Unknown request: " + s);
            }
        }
    }

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

    protected String[]
    getparamset(String key)
            throws IOException
    {
        String[] values = this.params.get(key);
        return (values == null ? new String[0] : values);
    }

    public String
    getparam(String key)
            throws IOException
    {
        String[] values = getparamset(key);
        if(values.length == 0) return null;
        String value = URLDecoder.decode(values[0], "UTF-8");
        value = HTTPUtil.nullify(value);
        return value;
    }

}


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

import ucar.nc2.constants.CDM;
import ucar.nc2.util.Indent;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Formatter;
import java.util.List;
import java.util.ArrayList;

/**
 * Concrete implementation of ArrayStructure, data storage is in a ByteBuffer, which is converted to member data on the fly.
 * In order to use this, the records must have the same size, and the member offset must be the same for each record.
 * Use StructureMembers.setStructureSize() to set the record size.
 * Use StructureMembers.Member.setDataParam() to set the offset of the member from the start of each record.
 * The member data will then be located in the BB at offset = recnum * getStructureSize() + member.getDataParam().
 * This defers object creation for efficiency. Use getArray<type>() and getScalar<type>() data accessors if possible.
 *

     Structure pdata = (Structure) ncfile.findVariable( name);
     StructureMembers members = pdata.makeStructureMembers();
     members.findMember("value").setDataParam(0); // these are the offsets into the record
     members.findMember("x_start").setDataParam(2);
     members.findMember("y_start").setDataParam(4);
     members.findMember("direction").setDataParam(6);
     members.findMember("speed").setDataParam(8);
     int recsize = pos[1] - pos[0]; // each record  must be all the same size
     members.setStructureSize( recsize);
     ArrayStructureBB asbb = new ArrayStructureBB( members, new int[] { size}, bos, pos[0]);
 *

 * For String members, you must store the Strings in the stringHeap. An integer index into the heap is used in the ByteBuffer.
 * @author caron
 * @see Array
 */
public class ArrayStructureBB extends ArrayStructure {

  /**
   * Set the offsets, based on m.getSizeBytes().
   * Also sets members.setStructureSize().
   * @param members set offsets for these members
   * @return the total size
   */
  public static int setOffsets(StructureMembers members) {
    int offset = 0;
    for (StructureMembers.Member m : members.getMembers()) {
      m.setDataParam(offset);
      offset += m.getSizeBytes();

      // set inner offsets (starts again at 0)
      if (m.getStructureMembers() != null)
        setOffsets(m.getStructureMembers());
    }
    members.setStructureSize(offset);
    return offset;
  }

  public static int showOffsets(StructureMembers members, Indent indent, Formatter f) {
    int offset = 0;
    for (StructureMembers.Member m : members.getMembers()) {
      f.format("%s%s offset=%d (%d %s = %d bytes)%n", indent, m.getName(), m.getDataParam(), m.getSize(), m.getDataType(), m.getSizeBytes());

      if (m.getStructureMembers() != null) {
        indent.incr();
        StructureMembers nested = m.getStructureMembers();
        f.format("%n%s%s == %d bytes%n", indent, nested.getName(), nested.getStructureSize());
        showOffsets(nested, indent, f);
        indent.decr();
      }
    }
    return offset;
  }

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

  protected ByteBuffer bbuffer;
  protected int bb_offset = 0;

  /**
   * Create a new Array of type StructureData and the given members and shape.
   * Generally, you extract the byte array and fill it:

     byte [] result = (byte []) structureArray.getStorage();

   *
   * @param members a description of the structure members
   * @param shape   the shape of the Array.
   */
  public ArrayStructureBB(StructureMembers members, int[] shape) {
    super(members, shape);
    this.bbuffer = ByteBuffer.allocate(nelems * getStructureSize());
    bbuffer.order(ByteOrder.BIG_ENDIAN);
  }

  /**
   * Construct an ArrayStructureBB with the given ByteBuffer.
   *
   * @param members the list of structure members.
   * @param shape   the shape of the structure array
   * @param bbuffer the data is stored in this ByteBuffer. bbuffer.order must already be set.
   * @param offset  offset from the start of the ByteBufffer to the first record.
   */
  public ArrayStructureBB(StructureMembers members, int[] shape, ByteBuffer bbuffer, int offset) {
    super(members, shape);
    this.bbuffer = bbuffer;
    this.bb_offset = offset;
  }

  @Override
  protected StructureData makeStructureData(ArrayStructure as, int index) {
    return new StructureDataA(as, index);
  }

  /**
   * Return backing storage as a ByteBuffer
   * @return backing storage as a ByteBuffer
   */
  public ByteBuffer getByteBuffer() {
    return bbuffer;
  }

  @Override
  public double getScalarDouble(int recnum, StructureMembers.Member m) {
    if (m.getDataType() != DataType.DOUBLE) throw new IllegalArgumentException("Type is " + m.getDataType() + ", must be double");
    if (m.getDataArray() != null) return super.getScalarDouble(recnum, m);

    int offset = calcOffsetSetOrder(recnum, m);
    return bbuffer.getDouble(offset);
  }

  @Override
  public double[] getJavaArrayDouble(int recnum, StructureMembers.Member m) {
    if (m.getDataType() != DataType.DOUBLE) throw new IllegalArgumentException("Type is " + m.getDataType() + ", must be double");
    if (m.getDataArray() != null) return super.getJavaArrayDouble(recnum, m);

    int offset = calcOffsetSetOrder(recnum, m);
    int count = m.getSize();
    double[] pa = new double[count];
    for (int i = 0; i < count; i++)
      pa[i] = bbuffer.getDouble(offset + i * 8);
    return pa;
  }

  @Override
  protected void copyDoubles(int recnum, StructureMembers.Member m, IndexIterator result) {
    int offset = calcOffsetSetOrder(recnum, m);
    int count = m.getSize();
    for (int i = 0; i < count; i++)
      result.setDoubleNext( bbuffer.getDouble(offset + i * 8));
  }

  @Override
  public float getScalarFloat(int recnum, StructureMembers.Member m) {
    if (m.getDataType() != DataType.FLOAT) throw new IllegalArgumentException("Type is " + m.getDataType() + ", must be float");
    if (m.getDataArray() != null) return super.getScalarFloat(recnum, m);

    int offset = calcOffsetSetOrder(recnum, m);
    return bbuffer.getFloat(offset);
  }

  @Override
  public Array getArray(int recnum, StructureMembers.Member m) {
    if (m.isVariableLength()) {
      int offset = calcOffsetSetOrder(recnum, m);
      int heapIndex = bbuffer.getInt(offset);
      return (Array) heap.get( heapIndex);
    }

    return super.getArray(recnum, m);
  }

  @Override
  public float[] getJavaArrayFloat(int recnum, StructureMembers.Member m) {
    if (m.getDataType() != DataType.FLOAT) throw new IllegalArgumentException("Type is " + m.getDataType() + ", must be float");
    if (m.getDataArray() != null) return super.getJavaArrayFloat(recnum, m);

    int offset = calcOffsetSetOrder(recnum, m);
    int count = m.getSize();
    float[] pa = new float[count];
    for (int i = 0; i < count; i++)
      pa[i] = bbuffer.getFloat(offset + i * 4);
    return pa;
  }

  @Override
  protected void copyFloats(int recnum, StructureMembers.Member m, IndexIterator result) {
    int offset = calcOffsetSetOrder(recnum, m);
    int count = m.getSize();
    for (int i = 0; i < count; i++)
      result.setFloatNext( bbuffer.getFloat(offset + i * 4));
  }

  @Override
  public byte getScalarByte(int recnum, StructureMembers.Member m) {
    if (!(m.getDataType().getPrimitiveClassType() == byte.class))
      throw new IllegalArgumentException("Type is "+m.getDataType()+", must be byte");
    if (m.getDataArray() != null) return super.getScalarByte(recnum, m);

    int offset = calcOffsetSetOrder(recnum, m);
    return bbuffer.get(offset);
  }

  @Override
  public byte[] getJavaArrayByte(int recnum, StructureMembers.Member m) {
    if (!(m.getDataType().getPrimitiveClassType() == byte.class))
      throw new IllegalArgumentException("Type is "+m.getDataType()+", must be byte");
    if (m.getDataArray() != null) return super.getJavaArrayByte(recnum, m);

    int offset = calcOffsetSetOrder(recnum, m);
    int count = m.getSize();
    byte[] pa = new byte[count];
    for (int i = 0; i < count; i++)
      pa[i] = bbuffer.get(offset + i);
    return pa;
  }

  @Override
  protected void copyBytes(int recnum, StructureMembers.Member m, IndexIterator result) {
    int offset = calcOffsetSetOrder(recnum, m);
    int count = m.getSize();
    for (int i = 0; i < count; i++)
      result.setByteNext( bbuffer.get(offset + i));
  }

  @Override
  public short getScalarShort(int recnum, StructureMembers.Member m) {
    if (!(m.getDataType().getPrimitiveClassType() == short.class))
      throw new IllegalArgumentException("Type is "+m.getDataType()+", must be short");
    if (m.getDataArray() != null) return super.getScalarShort(recnum, m);

    int offset = calcOffsetSetOrder(recnum, m);
    return bbuffer.getShort(offset);
  }

  @Override
  public short[] getJavaArrayShort(int recnum, StructureMembers.Member m) {
    if (!(m.getDataType().getPrimitiveClassType() == short.class))
      throw new IllegalArgumentException("Type is "+m.getDataType()+", must be short");
    if (m.getDataArray() != null) return super.getJavaArrayShort(recnum, m);

    int offset = calcOffsetSetOrder(recnum, m);
    int count = m.getSize();
    short[] pa = new short[count];
    for (int i = 0; i < count; i++)
      pa[i] = bbuffer.getShort(offset + i * 2);
    return pa;
  }

  @Override
  protected void copyShorts(int recnum, StructureMembers.Member m, IndexIterator result) {
    int offset = calcOffsetSetOrder(recnum, m);
    int count = m.getSize();
    for (int i = 0; i < count; i++)
      result.setShortNext(  bbuffer.getShort(offset + i * 2));
  }

  @Override
  public int getScalarInt(int recnum, StructureMembers.Member m) {
    if (!(m.getDataType().getPrimitiveClassType() == int.class))
      throw new IllegalArgumentException("Type is "+m.getDataType()+", must be int");
    if (m.getDataArray() != null) return super.getScalarInt(recnum, m);

    int offset = calcOffsetSetOrder(recnum, m);
    return bbuffer.getInt(offset);
  }

  @Override
  public int[] getJavaArrayInt(int recnum, StructureMembers.Member m) {
    if (!(m.getDataType().getPrimitiveClassType() == int.class))
      throw new IllegalArgumentException("Type is "+m.getDataType()+", must be int");
    if (m.getDataArray() != null) return super.getJavaArrayInt(recnum, m);

    int offset = calcOffsetSetOrder(recnum, m);
    int count = m.getSize();
    int[] pa = new int[count];
    for (int i = 0; i < count; i++)
      pa[i] = bbuffer.getInt(offset + i * 4);
    return pa;
  }

  @Override
  protected void copyInts(int recnum, StructureMembers.Member m, IndexIterator result) {
    int offset = calcOffsetSetOrder(recnum, m);
    int count = m.getSize();
    for (int i = 0; i < count; i++)
      result.setIntNext(  bbuffer.getInt(offset + i * 4));
  }

  @Override
  public long getScalarLong(int recnum, StructureMembers.Member m) {
    if (!(m.getDataType().getPrimitiveClassType() == long.class))
      throw new IllegalArgumentException("Type is "+m.getDataType()+", must be long");
    if (m.getDataArray() != null) return super.getScalarLong(recnum, m);

    int offset = calcOffsetSetOrder(recnum, m);
    return bbuffer.getLong(offset);
  }

  @Override
  public long[] getJavaArrayLong(int recnum, StructureMembers.Member m) {
    if (!(m.getDataType().getPrimitiveClassType() == long.class))
      throw new IllegalArgumentException("Type is "+m.getDataType()+", must be long");
    if (m.getDataArray() != null) return super.getJavaArrayLong(recnum, m);

    int offset = calcOffsetSetOrder(recnum, m);
    int count = m.getSize();
    long[] pa = new long[count];
    for (int i = 0; i < count; i++)
      pa[i] = bbuffer.getLong(offset + i * 8);
    return pa;
  }

  @Override
  protected void copyLongs(int recnum, StructureMembers.Member m, IndexIterator result) {
    int offset = calcOffsetSetOrder(recnum, m);
    int count = m.getSize();
    for (int i = 0; i < count; i++)
      result.setLongNext(  bbuffer.getLong(offset + i * 8));
  }

  @Override
  public char getScalarChar(int recnum, StructureMembers.Member m) {
    if (m.getDataType() != DataType.CHAR) throw new IllegalArgumentException("Type is " + m.getDataType() + ", must be char");
    if (m.getDataArray() != null) return super.getScalarChar(recnum, m);

    int offset = calcOffsetSetOrder(recnum, m);
    return (char) bbuffer.get(offset);
  }

  @Override
  public char[] getJavaArrayChar(int recnum, StructureMembers.Member m) {
    if (m.getDataType() != DataType.CHAR) throw new IllegalArgumentException("Type is " + m.getDataType() + ", must be char");
    if (m.getDataArray() != null) return super.getJavaArrayChar(recnum, m);

    int offset = calcOffsetSetOrder(recnum, m);
    int count = m.getSize();
    char[] pa = new char[count];
    for (int i = 0; i < count; i++)
      pa[i] = (char) bbuffer.get(offset + i);
    return pa;
  }

  @Override
  protected void copyChars(int recnum, StructureMembers.Member m, IndexIterator result) {
    int offset = calcOffsetSetOrder(recnum, m);
    int count = m.getSize();
    for (int i = 0; i < count; i++)
      result.setByteNext(  bbuffer.get(offset + i));
  }

  @Override
  public String getScalarString(int recnum, StructureMembers.Member m) {
    if (m.getDataArray() != null) return super.getScalarString(recnum, m);

    // strings are stored on the "heap", and the index to the heap is kept in the bbuffer
    if (m.getDataType() == DataType.STRING) {
      int offset = calcOffsetSetOrder(recnum, m);
      int index = bbuffer.getInt(offset);
      Object data = heap.get(index);
      if (data instanceof String) return (String) data;
      return ((String[]) data)[0];
    }

    if (m.getDataType() == DataType.CHAR) {
      int offset = calcOffsetSetOrder(recnum, m);
      int count = m.getSize();
      byte[] pa = new byte[count];
      int i;
      for (i = 0; i < count; i++) {
        pa[i] = bbuffer.get(offset + i);
        if (0 == pa[i]) break;
      }
      return new String(pa, 0, i, CDM.utf8Charset);
    }

    throw new IllegalArgumentException("Type is " + m.getDataType() + ", must be String or char");
  }

  @Override
  public String[] getJavaArrayString(int recnum, StructureMembers.Member m) {
    if (m.getDataArray() != null) return super.getJavaArrayString(recnum, m);

    // strings are stored on the "heap", and the index to the heap is kept in the bbuffer
    if (m.getDataType() == DataType.STRING) {
      int offset = calcOffsetSetOrder(recnum, m);
      int heapIndex = bbuffer.getInt(offset);
      Object ho = heap.get( heapIndex);
      if (ho instanceof String[])
        return (String[]) ho;
      else if (ho instanceof String) {
        String[] result = new String[1];
        result[0] = (String) ho;
        return result;
      } else {
        throw new IllegalArgumentException("Expected a String, but found an object of type  " + ho.getClass().getName() + ", on heap for "+
          " member "+m.getName());
      }
    }

    if (m.getDataType() == DataType.CHAR) {
      int[] shape = m.getShape();
      int rank = shape.length;
      if (rank < 2) {
        String[] result = new String[1];
        result[0] = getScalarString(recnum, m);
        return result;
      }

      int strlen = shape[rank - 1];
      int n = m.getSize() / strlen;
      int offset = calcOffsetSetOrder(recnum, m);
      String[] result = new String[n];
      for (int i = 0; i < n; i++) {
        byte[] bytes = new byte[strlen];
        for (int j = 0; j < bytes.length; j++)
          bytes[j] = bbuffer.get(offset + i * strlen + j);
        result[i] = new String(bytes, CDM.utf8Charset);
      }
      return result;
    }

    throw new IllegalArgumentException("Type is " + m.getDataType() + ", must be char");
  }

  @Override
  protected void copyObjects(int recnum, StructureMembers.Member m, IndexIterator result) {
    int offset = calcOffsetSetOrder(recnum, m);
    int count = m.getSize();
    int index = bbuffer.getInt(offset);
    String[] data = (String[]) heap.get(index);

    for (int i = 0; i < count; i++)
      result.setObjectNext(  data[i]);
  }

  // LOOK not tested ??
  @Override
  public StructureData getScalarStructure(int recnum, StructureMembers.Member m) {
    if (m.getDataType() != DataType.STRUCTURE) throw new IllegalArgumentException("Type is " + m.getDataType() + ", must be Structure");
    if (m.getDataArray() != null) return super.getScalarStructure(recnum, m);

    int offset = calcOffsetSetOrder(recnum, m);
    ArrayStructureBB subset = new ArrayStructureBB(m.getStructureMembers(), new int[]{1}, this.bbuffer, offset);

    return new StructureDataA(subset, 0);
  }

  @Override
  public ArrayStructure getArrayStructure(int recnum, StructureMembers.Member m) {
    if (m.getDataType() != DataType.STRUCTURE) throw new IllegalArgumentException("Type is " + m.getDataType() + ", must be Structure");
    if (m.getDataArray() != null) return super.getArrayStructure(recnum, m);

    int offset = calcOffsetSetOrder(recnum, m);
    ArrayStructureBB result = new ArrayStructureBB(m.getStructureMembers(), m.getShape(), this.bbuffer, offset);
    result.heap = this.heap; // share the heap
    return result;
  }

  @Override
  public ArraySequence getArraySequence(int recnum, StructureMembers.Member m) {
    if (m.getDataType() != DataType.SEQUENCE) throw new IllegalArgumentException("Type is " + m.getDataType() + ", must be Sequence");
    //if (m.getDataArray() != null) return super.getArrayStructure(recnum, m);

    int offset = calcOffsetSetOrder(recnum, m);
    int index = bbuffer.getInt(offset);
    if (heap == null) {
      System.out.println("ArrayStructureBB null heap");
      return null;
    }
    Object ho = heap.get(index);
    if (ho instanceof ArraySequence)
      return (ArraySequence) heap.get(index);
    return null; // LOOK
  }

  /* from the recnum-th structure, copy the member data into result.
  // member data is itself a structure, and may be an array of structures.
  @Override
  protected void copyStructures(int recnum, StructureMembers.Member m, IndexIterator result) {
    ArrayStructure data = getArrayStructure( recnum, m);
    Array data = getArray(recnum, m);
    IndexIterator dataIter = data.getIndexIterator();
    while (dataIter.hasNext())
      result.setObjectNext( dataIter.getObjectNext());

    int count = m.getSize();
    for (int i = 0; i < count; i++)
      result.setObjectNext(  makeStructureData(this, recnum));
  } */

  protected int calcOffsetSetOrder(int recnum, StructureMembers.Member m) {
    if (null != m.getDataObject())
      bbuffer.order( (ByteOrder) m.getDataObject());
    return bb_offset + recnum * getStructureSize() + m.getDataParam();
  }

  /*  int index = asbb.addObjectToHeap(s);
      bb.order( ByteOrder.nativeOrder()); // the string index is always written in "native order"
      bb.putInt(destPos + i * 4, index); // overwrite with the index into the StringHeap
  */
  private List<Object> heap;
  public int addObjectToHeap(Object s) {
    if (null == heap) heap = new ArrayList<>();
    heap.add(s);
    return heap.size() - 1;
  }

  public void addObjectToHeap(int recnum, StructureMembers.Member m, Object s) {
    if (null == heap) heap = new ArrayList<>();
    heap.add(s);
    int index = heap.size() - 1;
    // was  setInt(calcOffsetSetOrder(recnum, m), index) jc 12/4/2012
    bbuffer.putInt(calcOffsetSetOrder(recnum, m), index);
  }

  /**
   * DO NOT MODIFY
   * @return heap
   */
  public List<Object> getHeap() {
    return heap;
  }

  @Override
  public void showInternal(Formatter f, Indent indent) {
    super.showInternal(f, indent);

    f.format("%sByteBuffer = %s (hash=0x%x)%n", indent,  bbuffer, bbuffer.hashCode());
    if (null != heap) {
      f.format("%s  Heap Objects%n", indent);
      for (int i=0; i<heap.size(); i++) {
        Object o =  heap.get(i);
        f.format("%s   %d class=%s hash=0x%x = %s%n", indent, i, o.getClass().getName(), o.hashCode(), o);
        if (o instanceof ArrayStructure) {
          ((ArrayStructure) o).showInternal(f, indent.incr());
          indent.decr();
        }
      }
      f.format("%n");
    }

  }

  ////////////////////////////////////////////////////////////////////////
  // debugging
  public static void main(String argv[]) {
    byte[] ba = new byte[20];
    for (int i = 0; i < ba.length; ++i)
      ba[i] = (byte) i;

    ByteBuffer bbw = ByteBuffer.wrap(ba, 5, 15);
    bbw.get(0);
    System.out.println(" bbw(0)=" + bbw.get(0) + " i would expect = 5");

    bbw.position(5);
    System.out.println(" bbw(0)=" + bbw.get(0) + " i would expect = 4");
  }

}


package ucar.nc2.ft2.simpgeometry;

import java.util.List;

import org.slf4j.LoggerFactory;
import org.slf4j.Logger;

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

import ucar.ma2.Array;
import ucar.ma2.IndexIterator;
import ucar.ma2.InvalidRangeException;
import ucar.nc2.Variable;
import ucar.nc2.constants.CF;
import ucar.nc2.dataset.CoordinateAxis;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.ft2.simpgeometry.exception.InvalidDataseriesException;

/**
 * A CF 1.8 compliant Polygon
 * for use with Simple Geometries.
 * Can also represent multipolygons.
 *
 * @author wchen@usgs.gov
 *
 */
public class CFPolygon implements Polygon  {

	private Logger cfpl = LoggerFactory.getLogger(CFPolygon.class);
	private List<Point> points;	// a list of the constitutent points of the Polygon, connected in ascending order as in the CF convention
	private Polygon next;	// if non-null, next refers to the next line part of a multi-polygon
	private Polygon prev;	// if non-null, prev refers to the previous line part of a multi-polygon
	private boolean isInteriorRing; // true is an interior ring polygon otherwise false
	private Array data;	// data array associated with the polygon

	/**
	 * Get the geometry the data is associated with.
	 *
	 */

	public GeometryType getGeometryType() {
		return GeometryType.POLYGON;
	}

	/**
	 * Get the list of points which constitute the polygon.
	 *
	 * @return points
	 */
	public List<Point> getPoints() {
		return points;
	}

	/**
	 * Get the data associated with this Polygon
	 *
	 * @return data
	 */
	public Array getData() {
		return data;
	}

	/**
	 * Get the next polygon in the sequence of multi-polygons
	 *
	 * @return next polygon in the same multipolygon if any, otherwise null
	 */
	public Polygon getNext() {
		return next;
	}

	/**
	 * Get the previous polygon in the sequence of multi-polygons
	 *
	 * @return previous polygon in the same multipolygon if any, otherwise null
	 */
	public Polygon getPrev() {
		return prev;
	}

	/**
	 * Get whether or not this polygon is an interior ring
	 *
	 * @return true if an interior ring, false if not
	 */
	public boolean getInteriorRing() {
		return isInteriorRing;
	}

	/**
	 * Add a point to this polygon's points list
	 *
	 */
	public void addPoint(double x, double y) {
		Point ptPrev = null;

		if(points.size() > 0) {
			ptPrev = points.get(points.size() - 1);
		}

		this.points.add(new CFPoint(x, y, ptPrev, null, null));
	}

	/**
	 * Set the data associated with this Polygon
	 *
	 * @param data - array of data to set to
	 */
	public void setData(Array data) {
		this.data = data;
	}

	/**
	 * Sets the next polygon which make up the multipolygon which this polygon is a part of.
	 * If next is a CFPolygon, automatically connects the other polygon to this polygon as well.
	 */
	public void setNext(Polygon next) {
		if(next instanceof CFPolygon) {
			setNext((CFPolygon) next);
		}

		else this.next = next;
	}

	/**
	 * Sets the next polygon which make up the multipolygon which this polygon is a part of.
	 * Automatically connects the other polygon to this polygon as well.
	 */
	protected void setNext(CFPolygon next) {
		this.next = next;

		if(next != null) {
			next.setPrevOnce(this);
		}
	}

	private void setNextOnce(CFPolygon next) {
		this.next = next;
	}

	/**
	 * Sets the previous polygon which makes up the multipolygon which this polygon is a part of.
	 * If prev is a CFPolygon, automatically connect the other polygon to this polygon as well.
	 */
	public void setPrev(Polygon prev) {

		if(prev instanceof CFPolygon) {
			setPrev((CFPolygon) prev);
		}

		else this.prev = prev;
	}

	/**
	 * Sets the previous polygon which makes up the multipolygon which this polygon is a part of.
	 * Automatically connect the other polygon to this polygon as well.
	 */
	public void setPrev(CFPolygon prev) {
		this.prev = prev;

		if(prev != null) {
			prev.setNextOnce(this);
		}
	}

	private void setPrevOnce(CFPolygon prev) {
		this.prev = prev;
	}

	/**
	 *  Sets whether or not this polygon is an interior ring.
	 *
	 */
	public void setInteriorRing(boolean interior) {
		this.isInteriorRing = interior;
	}

	/**
	 * Given a dataset, variable and index, automatically sets up a previously constructed polygon.
	 * If the specified polygon is not found in the dataset, returns null
	 *
	 * @param dataset which the variable is a part of
	 * @param polyvar the variable which has a geometry attribute
	 * @param index of the polygon within the variable
	 *
	 */
	public Polygon setupPolygon(NetcdfDataset dataset, Variable polyvar, int index)
	{
		this.points.clear();
		Array xPts = null;
		Array yPts = null;
		Variable nodeCounts = null;
		Variable partNodeCounts = null;
		Variable interiorRings = null;

		List<CoordinateAxis> axes = dataset.getCoordinateAxes();
		CoordinateAxis x = null; CoordinateAxis y = null;

		String[] nodeCoords = polyvar.findAttributeIgnoreCase(CF.NODE_COORDINATES).getStringValue().split(" ");

		// Look for x and y

		for(CoordinateAxis ax : axes){

			if(ax.getFullName().equals(nodeCoords[0])) x = ax;
			if(ax.getFullName().equals(nodeCoords[1])) y = ax;
		}

		// Affirm node counts
		String nodeCoStr = polyvar.findAttValueIgnoreCase(CF.NODE_COUNT, "");

		if(!nodeCoStr.equals("")) {
			nodeCounts = dataset.findVariable(nodeCoStr);
		}

		else return null;

		// Affirm part node counts
		String pNodeCoStr = polyvar.findAttValueIgnoreCase(CF.PART_NODE_COUNT, "");

		if(!pNodeCoStr.equals("")) {
			partNodeCounts = dataset.findVariable(pNodeCoStr);
		}

		// Affirm interior rings
		String interiorRingsStr = polyvar.findAttValueIgnoreCase(CF.PART_NODE_COUNT, "");

		if(!interiorRingsStr.equals("")) {
				interiorRings = dataset.findVariable(interiorRingsStr);
		}

		SimpleGeometryIndexFinder indexFinder = new SimpleGeometryIndexFinder(nodeCounts);

		//Get beginning and ending indicies for this polygon
		int lower = indexFinder.getBeginning(index);
		int upper = indexFinder.getEnd(index);

		try {

			xPts = x.read( lower + ":" + upper ).reduce();
			yPts = y.read( lower + ":" + upper ).reduce();

			IndexIterator itrX = xPts.getIndexIterator();
			IndexIterator itrY = yPts.getIndexIterator();

			// No multipolygons just read in the whole thing
			if(partNodeCounts == null) {

				this.next = null;
				this.prev = null;
				this.isInteriorRing = false;

				// x and y should have the same shape, will add some handling on this
				while(itrX.hasNext()) {
					this.addPoint(itrX.getDoubleNext(), itrY.getDoubleNext());
				}

				switch(polyvar.getRank()) {

				case 2:
					this.setData(polyvar.read(CFSimpleGeometryHelper.getSubsetString(polyvar, index)).reduce());
					break;

				case 1:
					this.setData(polyvar.read("" + index));
					break;

				default:
					throw new InvalidDataseriesException(InvalidDataseriesException.RANK_MISMATCH);	// currently do not support anything but dataseries and scalar associations

				}
			}

			// If there are multipolygons then take the upper and lower of it and divy it up
			else {

				Polygon tail = this;
				Array pnc = partNodeCounts.read();
				Array ir = null;
				IndexIterator pncItr = pnc.getIndexIterator();

				if(interiorRings != null) ir = interiorRings.read();

				// In part node count search for the right index to begin looking for "part node counts"
				int pncInd = 0;
				int pncEnd = 0;
				while(pncEnd < lower)
				{
					pncEnd += pncItr.getIntNext();
					pncInd++;
				}

				// Now the index is found, use part node count and the index to find each part node count of each individual part
				while(lower < upper) {

					int smaller = pnc.getInt(pncInd);

					// Set interior ring if needed
					if(interiorRings != null)
					{
						int interiorRingValue = ir.getInt(pncInd);

						switch(interiorRingValue) {

							case 0:
								this.setInteriorRing(false);
								break;

							case 1:
								this.setInteriorRing(true);
								break;

							// will handle default case
						}

					} else this.isInteriorRing = false;

					while(smaller > 0) {
						tail.addPoint(itrX.getDoubleNext(), itrY.getDoubleNext());
						smaller--;
					}

					// Set data of each
					switch(polyvar.getRank()) {

					case 2:
						tail.setData(polyvar.read(CFSimpleGeometryHelper.getSubsetString(polyvar, index)).reduce());
						break;

					case 1:
						tail.setData(polyvar.read("" + index));
						break;

					default:
						throw new InvalidDataseriesException(InvalidDataseriesException.RANK_MISMATCH);	// currently do not support anything but dataseries and scalar associations

					}

					lower += tail.getPoints().size();
					pncInd++;
					tail.setNext(new CFPolygon());
					tail = tail.getNext();
				}

				//Clean up
				tail = tail.getPrev();
				if(tail != null) tail.setNext(null);
			}
		}

		catch (IOException | InvalidRangeException | InvalidDataseriesException e) {
			cfpl.error(e.getMessage());
			return null;
		}

		return this;
	}

	/**
	 * Gets the upper bounding box coordinate on the polygon.
	 * @return double array = (x, y)
	 */
	public double[] getBBUpper() {
		double[] bbUpper = new double[2];

		List<Point> ptList = this.getPoints();
		if(ptList.isEmpty()) return null;
		bbUpper[0] = ptList.get(0).getY();
		bbUpper[1] = ptList.get(0).getY();

		for(Point pt : this.getPoints()) {
			if(bbUpper[0] < pt.getX()){
				bbUpper[0] = pt.getX();
			}

			if(bbUpper[1] < pt.getY()) {
				bbUpper[1] = pt.getY();
			}
		}

		// Got maximum points, add some padding.
		bbUpper[0] += 10;
		bbUpper[1] += 10;

		return bbUpper;
	}

	/**
	 * Gets the lower bounding box coordinate on the polygon.
	 * @return double array = (x, y)
	 */
	public double[] getBBLower() {
		double[] bbLower = new double[2];

		List<Point> ptList = this.getPoints();
		if(ptList.isEmpty()) return null;
		bbLower[0] = ptList.get(0).getY();
		bbLower[1] = ptList.get(0).getY();

		for(Point pt : this.getPoints()) {
			if(bbLower[0] > pt.getX()){
				bbLower[0] = pt.getX();
			}

			if(bbLower[1] > pt.getY()) {
				bbLower[1] = pt.getY();
			}
		}

		// Got minimum points, add some padding.
		bbLower[0] -= 10;
		bbLower[1] -= 10;

		return bbLower;
	}

	/**
	 * Constructs an empty polygon with nothing in it using an Array List.
	 */
	public CFPolygon() {
		this.points = new ArrayList<Point>();
		this.next = null;
		this.prev = null;
		this.isInteriorRing = false;
		this.data = null;
	}

	/**
	 * Constructs a new polygon whose points constitute the points passed in.
	 *
	 * @param points which make up the Polygon
	 */
	public CFPolygon(List<Point> points) {
		this.points = points;
		this.next = null;
		this.prev = null;
		this.isInteriorRing = false;
		this.data = null;
	}
}


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

package thredds.catalog;

import java.util.*;

/**
 * A Service is an abstraction for an internet service, such as a data server, FTP, etc.
 *
 * @author john caron
 */

public class InvService {
  // standard TDS services
  public static final InvService cdmremote = new InvService("cdmremote", ServiceType.CdmRemote.toString(), "/thredds/cdmremote/", "", "");
  public static final InvService cdmrfeature = new InvService("cdmrfeature", ServiceType.CdmrFeature.toString(), "/thredds/cdmrfeature/", "", "");
  public static final InvService fileServer = new InvService("fileServer", ServiceType.HTTPServer.toString(), "/thredds/fileServer/", "", "");
  public static final InvService latest = new InvService("latest", ServiceType.RESOLVER.toString(), "", "", "");
  public static final InvService ncssGrid = new InvService("ncssGrid", ServiceType.NetcdfSubset.toString(), "/thredds/ncss/grid/", "", "");
  public static final InvService ncssPoint = new InvService("ncssPoint", ServiceType.NetcdfSubset.toString(), "/thredds/ncss/point/", "", "");
  public static final InvService opendap = new InvService("opendap", ServiceType.OPENDAP.toString(), "/thredds/dodsC/", "", "");
  public static final InvService dap4 = new InvService("dap4", ServiceType.DAP4.toString(), "/thredds/dap4/", "", "");
  public static final InvService wcs = new InvService("wcs", ServiceType.WCS.toString(), "/thredds/wcs/", "", "");
  public static final InvService wms = new InvService("wms", ServiceType.WMS.toString(), "/thredds/wms/", "", "");

  //NGDC addition 5/10/2011
  public static final InvService ncml = new InvService("ncml", ServiceType.NCML.toString(), "/thredds/ncml/", "", "");
  public static final InvService uddc = new InvService("uddc", ServiceType.UDDC.toString(), "/thredds/uddc/", "", "");
  public static final InvService iso = new InvService("iso", ServiceType.ISO.toString(), "/thredds/iso/", "", "");
  public static final InvService ncJSON  = new InvService("ncjason", ServiceType.ncJSON.toString(), "/thredds/ncjason/", "", "");
  public static final InvService h5Service  = new InvService("h5service", ServiceType.H5Service.toString(), "/thredds/h5service/", "", "");

  //////////////////////////////////
  private String name;
  private String base;
  private String suffix;
  private String desc;
  private ServiceType type = null;

  private java.net.URI uri = null;
  private List<InvService> nestedServices = new ArrayList<>();
  private List<InvProperty> properties = new ArrayList<>();
  private List<InvProperty> roots = new ArrayList<>(); // deprecated

  private StringBuilder log = new StringBuilder();

  /**
   * Constructor.
   *
   * @param name            : name to show to the user
   * @param serviceTypeName : ServiceType
   * @param base            : base for forming URL
   * @param suffix          : suffix for forming URL, may be null.
   * @param desc            : human readable description, may be null.
   */
  public InvService(String name, String serviceTypeName, String base, String suffix, String desc) {
    this.name = name;
    this.type = ServiceType.findType(serviceTypeName);
    this.base = (base == null) ? "" : base.trim();
    this.suffix = (suffix == null) ? "" : suffix.trim();
    this.desc = desc;

    // deal with strange service types
    if (type == null) {
      log.append(" ** InvService: non-standard type =(").append(serviceTypeName).append(") for service (").append(name).append(")");
      type = ServiceType.getType(serviceTypeName);
    }

    if (name == null) {
      log.append(" ** InvService has no name");
    }

  }

  /**
   * Get the service name: referenced by dataset and access elements.
   * @return the service name
   */
  public String getName() {
    return name;
  }

  /**
   * get the base URL for the service
   * @return the base URL for the service
   */
  public String getBase() {
    return base;
  }

  /**
   * get the Service Type
   * @return the Service Type
   */
  public ServiceType getServiceType() {
    return type;
  }

  /**
   * Get the suffix; may be null
   * @return the suffix; may be null
   */
  public String getSuffix() {
    return suffix;
  }

  /**
   * Get the "human readable" description; use ServiceType.toString() if not set
   * @return the "human readable" description
   */
  public String getDescription() {
    return (desc != null) ? desc : type.toString();
  }

  /**
   * Get properties for this service.
   *
   * @return List of type Property. May be empty, but not null.
   */
  public java.util.List<InvProperty> getProperties() {
    return properties;
  }

  /**
   * Get dataset roots.
   *
   * @return List of InvProperty. May be empty, may not be null.
   * @deprecated put roots only in the catalog
   */
  public java.util.List<InvProperty> getDatasetRoots() {
    return roots;
  }

  /**
   * Get named property.
   *
   * @param name match this name
   * @return String value of Property or null if not exist.
   */
  public String findProperty(String name) {
    InvProperty result = null;
    for (InvProperty p : properties) {
      if (p.getName().equals(name))
        result = p;
    }
    return (result == null) ? null : result.getValue();
  }

  public InvService findNestedService(String name) {
    for (InvService s : nestedServices) {
      if (s.getName().equals(name)) return s;
      if (s.type == ServiceType.COMPOUND) {
        InvService result = s.findNestedService(name);
        if (result != null) return result;
      }
    }
    return null;
  }

  @Override
  public String toString() {
    return "name:(" + name + ") type:(" + type + ") base:(" + base + ") suffix:(" + suffix + ")";
  }

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

    InvService that = (InvService) o;

    if (!base.equals(that.base)) return false;
    if (!name.equals(that.name)) return false;
    if (nestedServices != null ? !nestedServices.equals(that.nestedServices) : that.nestedServices != null) return false;
    if (properties != null ? !properties.equals(that.properties) : that.properties != null) return false;
    if (suffix != null ? !suffix.equals(that.suffix) : that.suffix != null) return false;
    if (!type.equals(that.type)) return false;

    return true;
  }

  @Override
  public int hashCode() {
    int result = name.hashCode();
    result = 31 * result + base.hashCode();
    result = 31 * result + (suffix != null ? suffix.hashCode() : 0);
    result = 31 * result + type.hashCode();
    result = 31 * result + (nestedServices != null ? nestedServices.hashCode() : 0);
    result = 31 * result + (properties != null ? properties.hashCode() : 0);
    return result;
  }

  /**
   * InvServices with same values are equal
   *
  public boolean equals(Object o) {
    if (this == o)
      return true;
    if (!(o instanceof InvService))
      return false;
    return o.hashCode() == this.hashCode();
  }

  /**
   * Override Object.hashCode() to implement equals.
   *
  public int hashCode() {
    if (hashCode == 0) {
      int result = 17;
      result = 37 * result + getName().hashCode();
      result = 37 * result + getBase().hashCode();
      result = 37 * result + getServiceType().hashCode();
      if (null != getSuffix())
        result = 37 * result + getSuffix().hashCode();
      result = 37 * result + getProperties().hashCode();
      result = 37 * result + nestedServices.hashCode();
      hashCode = result;
    }
    return hashCode;
  }  */

  // private volatile int hashCode = 0; // Bloch, item 8

  /**
   * @return debugging info
   */
  public String dump() {
    return dump(0);
  }

  String dump(int n) {
    StringBuilder buff = new StringBuilder(200);
    buff.setLength(0);

    buff.append(InvDatasetImpl.indent(n)).append("Service ").append(this).append("\n");

      for (InvService s : getServices()) {
        buff.append(s.dump(n + 2));
    }

    List<InvProperty> props = getProperties();
    if (props.size() > 0) {
      String indent = InvDatasetImpl.indent(n + 2);
      buff.append(indent);
      buff.append("Properties:\n");
      for (InvProperty p : props) {
        buff.append(InvDatasetImpl.indent(n + 4)).append(p).append("\n");
      }
    }

    return buff.toString();
  }

  ////////////////////////////////////////////////////////////////////////////
  /**
   * Add a nested service to a service of type COMPOUND.
   * @param service add this
   */
  public void addService(InvService service) {
    nestedServices.add(service);
  }

  /**
   * Add a property
   * @param p add this
   */
  public void addProperty(InvProperty p) {
    properties.add(p);
  }

  /**
   * Add Dataset Root (1.0), key = path,  value = location.
   *
   * @param root add this
   * @deprecated use InvCatalogImpl
   */
  public void addDatasetRoot(InvProperty root) {
    roots.add(root);
  }

  /**
   * Get nested services; only if getServiceType() == ServiceType.COMPOUND.
   *
   * @return List of type InvService. May be empty, but not null.
   */
  public java.util.List<InvService> getServices() {
    return nestedServices;
  }

  /**
   * Get full name for this Service, which has all parent collection names.
   *
   * @deprecated services should always be at top level.
   * @return name
   */
  public String getFullName() {
    return name;
  }

  protected boolean check(StringBuilder out) {
    boolean isValid = true;
    if (log.length() > 0) {
      out.append(log);
    }

    // compound services
    if (getServiceType() == ServiceType.COMPOUND) {
      if (getServices().size() < 1) {
        out.append(" ** InvService (").append(getName()).append(") type COMPOUND must have a nested service\n");
        isValid = false;
      }
    } else {
      if (getServices().size() > 0) {
        out.append(" ** InvService(").append(getName()).append(") type ").append(getServiceType()).append(" may not have nested services\n");
        isValid = false;
      }
    }

    // check urlPath is ok
    try {
      uri = new java.net.URI(base);
    } catch (java.net.URISyntaxException e) {
      out.append(" ** InvService(").append(getName()).append(") invalid base URL =(").append(base).append(")");
      isValid = false;
    }

    return isValid;
  }

  /**
   * See if the service Base is reletive
   * @return true if the service Base is reletive
   */
  public boolean isRelativeBase() {
    if (getServiceType() == ServiceType.COMPOUND)
      return true;

    if (uri == null) {
      try {
        uri = new java.net.URI(base);
      } catch (java.net.URISyntaxException e) {
        throw new IllegalArgumentException(e.getMessage());
      }
    }

    return !uri.isAbsolute();
  }
}


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

import java.awt.Dimension;
import java.io.*;
import java.util.ArrayList;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;

import ucar.nc2.util.ListenerManager;
import ucar.unidata.geoloc.ProjectionImpl;
import ucar.unidata.geoloc.ProjectionRect;
import ucar.util.prefs.PreferencesExt;

/**
 *  Consider this a private inner class of ProjectionManager.
 *
 * @author John Caron
 * @version revived /20/2012
 */

public class JTableProjection extends JTable {
  private PreferencesExt store = null;
  private ProjectionTableModel model = null;
  private ArrayList list;
  private boolean debug = false;
  private int selectedRow = 0;  // JTable doesnt handle selections correctly
  private ListenerManager lm;

  private static final String STORE_NAME = "ProjectionTableModel";

  public JTableProjection( PreferencesExt pstore) {
    this.store = pstore;

    if (store == null)
      model = new ProjectionTableModel();
    else {
      model = (ProjectionTableModel) store.getObject( STORE_NAME);
      if (model == null)
        model = new ProjectionTableModel();
    }

    list = model.getList();

    setModel(model);
    setAutoResizeMode( JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS);
    setPreferredScrollableViewportSize( new Dimension(400, 200));
    getTableHeader().setReorderingAllowed(true);
    model.adjustColumns(getColumnModel());

    // manage the selection
    setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

    // have to manage selectedRow ourselves, due to bugs
    getSelectionModel().addListSelectionListener( new ListSelectionListener() {
      public void valueChanged(ListSelectionEvent e) {
        if (debug) System.out.println(" ListSelectionListener= "+ e);
        if (!e.getValueIsAdjusting()) {
          selectedRow = getSelectedRow();
          if (debug) System.out.println("     selectd= "+ selectedRow);
          lm.sendEvent(new NewProjectionEvent(this, getSelected()));
        }
      }
    });

    // manage NewProjectionListener's
    lm = new ListenerManager(
        "ucar.nc2.ui.geoloc.NewProjectionListener",
        "ucar.nc2.ui.geoloc.NewProjectionEvent",
        "actionPerformed");
  }

  public void addProjection(ProjectionImpl proj) {
    int rowno = model.addProjection(proj);
    setRowSelectionInterval(rowno, rowno);
    selectedRow = rowno;
  }

  public void replaceProjection(ProjectionImpl proj) {
    int rowno = model.replaceProjection(proj);
    setRowSelectionInterval(rowno, rowno);
    selectedRow = rowno;
  }

  public boolean contains(ProjectionImpl proj) {
    return (model.search(proj) >= 0);
  }

  public boolean contains(String id) {
    return (model.search(id) >= 0);
  }

  public ProjectionImpl getSelected() {
    int len = list.size();
    if ((0 > selectedRow) || (len <= selectedRow))
      return null;
    else
      return (ProjectionImpl) list.get(selectedRow);
  }

  public void deleteSelected() {
    int len = list.size();
    if ((0 > selectedRow) || (len <= selectedRow))
      return;

    model.deleteRow( selectedRow);

      // bugs in list selection code
    len = list.size();
    if (len == 0) {
      clearSelection();
      selectedRow = -1;
    } else {
      if (selectedRow > len-1) selectedRow = len-1;
      setRowSelectionInterval(selectedRow, selectedRow);
      if (debug) System.out.println(" set selection to "+selectedRow);
    }

    if (debug) System.out.println(" selection now= "+getSelectedRow()+ " really= "+ selectedRow);
    lm.sendEvent(new NewProjectionEvent(this, getSelected()));
    repaint();
  }

  public boolean isEmpty() {
    return (model.getRowCount() == 0);
  }

  public void storePersistentData() {
    if (store != null)
      store.putObject( STORE_NAME, model);
  }

  public void setMapArea( ProjectionRect bb) {
    if (0 > selectedRow)
      return;
    model.setMapArea(selectedRow, bb);
    if (debug) System.out.println(" PTsetMapArea = "+ bb+ " on "+ selectedRow);
  }

    // set current projection if found, else deselect
  public void setCurrentProjection(ProjectionImpl proj) {
    int row;
    if (0 <= (row = model.search(proj))) {
      if (debug) System.out.println(" PTsetCurrentProjection found = "+ row);
      selectedRow = row;
      setRowSelectionInterval(row, row);
    } else {
      if (debug) System.out.println(" PTsetCurrentProjection not found = "+ row);
      selectedRow = -1;
      clearSelection();
    }
  }

    // event listener managagment
  public void addNewProjectionListener( NewProjectionListener l) {
    lm.addListener(l);
  }
  public void removeNewProjectionListener( NewProjectionListener l) {
    lm.removeListener(l);
  }

  // inner class must be static because JTable not Serializable
  private static class ProjectionTableModel extends AbstractTableModel implements java.io.Serializable {
    private static String[] colName = {"Name", "Type", "Parameters", "Default Zoom"};
    private ArrayList list = new ArrayList(20);

    // AbstractTableModel methods
    public int getRowCount() { return list.size(); }
    public int getColumnCount() { return colName.length; }
    public String getColumnName(int col) { return colName[col]; }
    public Object getValueAt (int row, int col) {
      ProjectionImpl proj = (ProjectionImpl) list.get( row);
      switch (col) {
        case 0: return proj.getName();
        case 1: return proj.getClassName();
        case 2: return proj.paramsToString();
        case 3: return proj.getDefaultMapArea();
      }
      return "error";
    }
    public boolean isCellEditable(int rowIndex, int columnIndex) {
      return (columnIndex == 0);
    }
    public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
      ProjectionImpl proj = (ProjectionImpl) list.get( rowIndex);
      proj.setName( new String((String) aValue));
    }

      // do our own listener management to get around serialization bug
      // can delete this section when bug is fixed
    ProjectionTableModel() { constructLM(); }
    private transient ListenerManager lm;
    private void constructLM() {
      lm = new ListenerManager("javax.swing.event.TableModelListener",
       "javax.swing.event.TableModelEvent", "tableChanged");
    }
    public void addTableModelListener(TableModelListener l) {
      lm.addListener(l);
    }
    public void removeTableModelListener(TableModelListener l) {
      lm.removeListener(l);
    }
    public void fireTableChanged(TableModelEvent e) {
      lm.sendEvent(e);
    }
    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
      s.defaultReadObject();
      constructLM();
    }

    // added methods
    int addProjection(ProjectionImpl proj) {
      list.add(proj);
      int count = list.size() - 1;
      fireTableRowsInserted(count, count);
      return count;
    }

    int replaceProjection(ProjectionImpl proj) {
      int rowno = search( proj);
      if (rowno < 0)
        return -1;
      list.set( rowno, proj);
      return rowno;
    }

    void adjustColumns( TableColumnModel colModel) {
      for (int i=0; i<colName.length; i++) {
        colModel.getColumn(i).setMinWidth(50);
        colModel.getColumn(i).setPreferredWidth(100);
      }
    }

    ArrayList getList() { return list; }

    void deleteRow(int row) {
      int len = list.size();
      if (row < len)
        list.remove(row);
      else return;
      fireTableRowsDeleted(row, row);
    }

    int search( ProjectionImpl proj) {
      for (int row=0; row<list.size(); row++) {
        ProjectionImpl test = (ProjectionImpl) list.get( row);
        if (proj.getName().equals(test.getName()))
          return row;
      }
      return -1;
    }

    int search( String projName) {
      for (int row=0; row<list.size(); row++) {
        ProjectionImpl test = (ProjectionImpl) list.get( row);
        if (projName.equals(test.getName()))
          return row;
      }
      return -1;
    }

    void setMapArea( int row, ProjectionRect bb) {
      int len = list.size();
      if (row >= len)
        return;
      ProjectionImpl proj = (ProjectionImpl) list.get(row);
      proj.getDefaultMapArea().setRect( bb);
      fireTableRowsUpdated(row, row);
    }
  }
}


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

import ucar.nc2.time.CalendarDate;
import ucar.nc2.time.CalendarDateRange;
import ucar.nc2.util.Optional;

/**
 * A new way to handle 2D time, a runtime axis with orthogonal offset values, so time = (runtime x offset).
 * This class represents the offset values, which must be the same for each runtime.
 * A Time2DCoordSys has a runtime and a TimeOffsetAxis, and manages the 2D time.
 * @author John
 * @since 8/13/2015
 */
public class TimeOffsetAxis extends CoverageCoordAxis1D {

  public TimeOffsetAxis( CoverageCoordAxisBuilder builder) {
    super(builder);
  }

  @Override
  public CoverageCoordAxis copy() {
    return new TimeOffsetAxis(new CoverageCoordAxisBuilder(this));
  }

  // normal case already handled, this is the case where a time has been specified, and only one runtime
  public Optional<TimeOffsetAxis> subsetFromTime(SubsetParams params, CalendarDate runDate) {
    CoordAxisHelper helper = new CoordAxisHelper(this);
    CoverageCoordAxisBuilder builder = null;
    if (params.isTrue(SubsetParams.timePresent)) {
      double offset = getOffsetInTimeUnits(runDate, CalendarDate.present());
      builder = helper.subsetClosest(offset);
    }

    CalendarDate dateWanted = (CalendarDate) params.get(SubsetParams.time);
    if (dateWanted != null) {                           // convertFrom, convertTo
      double offset = getOffsetInTimeUnits(runDate, dateWanted);
      builder =  helper.subsetClosest(offset);
    }

    Integer stride = (Integer) params.get(SubsetParams.timeStride);
    if (stride == null || stride < 0) stride = 1;

    CalendarDateRange dateRange = (CalendarDateRange) params.get(SubsetParams.timeRange);
    if (dateRange != null) {
      double min = getOffsetInTimeUnits(runDate, dateRange.getStart());
      double max = getOffsetInTimeUnits(runDate, dateRange.getEnd());
      Optional<CoverageCoordAxisBuilder> buildero =  helper.subset(min, max, stride);
      if (buildero.isPresent()) builder = buildero.get();
      else return Optional.empty(buildero.getErrorMessage());
    }

    assert (builder != null);

    // all the offsets are reletive to rundate
    builder.setReferenceDate(runDate);
    return Optional.of(new TimeOffsetAxis(builder));
  }

  public CalendarDate makeDate(CalendarDate runDate, double val) {
    double offset = timeHelper.getOffsetInTimeUnits(timeHelper.getRefDate(), runDate);
    return timeHelper.makeDate(offset + val);
  }

  @Override
  public Optional<CoverageCoordAxis> subset(SubsetParams params) {
    Optional<CoverageCoordAxisBuilder> buildero = subsetBuilder(params);
    return !buildero.isPresent() ? Optional.empty(buildero.getErrorMessage()) : Optional.of(new TimeOffsetAxis(buildero.get()));
  }

  @Override
  public Optional<CoverageCoordAxis> subset(double minValue, double maxValue, int stride) {
    CoordAxisHelper helper = new CoordAxisHelper(this);
    Optional<CoverageCoordAxisBuilder> buildero = helper.subset(minValue, maxValue, stride);
    return !buildero.isPresent() ? Optional.empty(buildero.getErrorMessage()) : Optional.of(new TimeOffsetAxis(buildero.get()));
  }

}