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

package ucar.nc2.grib.grib2;

import javax.annotation.Nullable;
import ucar.nc2.grib.GribNumbers;
import ucar.nc2.time.CalendarDate;
import ucar.unidata.util.Format;
import ucar.unidata.util.StringUtil2;

import javax.annotation.concurrent.Immutable;
import java.util.Formatter;
import java.util.zip.CRC32;

/**
 * Abstract superclass for GRIB2 PDS handling.
 * Inner classes are specific to each template.
 *
 * @author caron
 * @since 3/28/11
 */

@Immutable
public abstract class Grib2Pds {
  private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Grib2Pds.class);

  /**
   * Factory for Grib2Pds
   *
   * @param template pds template number
   * @param input   raw bytes
   * @return Grib2Pds or null on error
   */
  @Nullable
  public static Grib2Pds factory(int template, byte[] input) {
    switch (template) {
      case 0:
        return new Grib2Pds0(input);
      case 1:
        return new Grib2Pds1(input);
      case 2:
        return new Grib2Pds2(input);
      case 5:
        return new Grib2Pds5(input);
      case 6:
        return new Grib2Pds6(input);
      case 8:
        return new Grib2Pds8(input);
      case 9:
        return new Grib2Pds9(input);
      case 10:
        return new Grib2Pds10(input);
      case 11:
        return new Grib2Pds11(input);
      case 12:
        return new Grib2Pds12(input);
      case 15:
        return new Grib2Pds15(input);
      case 30:
        return new Grib2Pds30(input);
      case 31:
        return new Grib2Pds31(input);
      case 48:
          return new Grib2Pds48(input);
      case 61:
          return new Grib2Pds61(input);
      default:
        log.warn("Missing template " + template);
        return null;
    }
  }

  /////////////////////////////////////////////////
  protected final byte[] input;
  protected final int template; // product definition template

  /**
   * Constructs a Grib2PDSVariables object from a byte[].
   *
   * @param input   raw bytes
   */
  protected Grib2Pds(byte[] input) {
    this.input = input;
    template = GribNumbers.int2(getOctet(8), getOctet(9));
  }

  // optional coordinates start after this
  public abstract int templateLength();

  /**
   * Number of coordinate values at end of template.
   *
   * @return Coordinates number
   */
  public int getExtraCoordinatesCount() {
    return GribNumbers.int2(getOctet(6), getOctet(7));
  }

  final float[] getExtraCoordinates() {
    int n = getExtraCoordinatesCount();
    if (n == 0) return new float[0];
    float[] result = new float[n];
    int count = templateLength() + 1;
    for (int i=0; i<n; i++) {
      result[i] = GribNumbers.float4(getOctet(count++),getOctet(count++),getOctet(count++),getOctet(count++));
    }
    return result;
  }

  /**
   * product Definition template, Table 4.0
   *
   * @return ProductDefinition
   */
  public final int getTemplateNumber() {
    return template;
  }

  /**
   * Parameter Category
   *
   * @return parameterCategory as int
   */
  public final int getParameterCategory() {
    return getOctet(10);
  }

  /**
   * Parameter Number
   *
   * @return ParameterNumber
   */
  public final int getParameterNumber() {
    return getOctet(11);
  }

  /**
   * Type of Generating Process (Code Table 4.3)
   *
   * @return Type of Generating Process
   */
  public int getGenProcessType() {
    return GribNumbers.UNDEFINED;
  }

  /**
   * Forecast/Analysis generating process identifier (defined by originating centre).
   * <p/>
   * For NCEP, apparently
   * http://www.nco.ncep.noaa.gov/pmb/docs/on388/tablea.html
   * as linked from here:
   * http://www.nco.ncep.noaa.gov/pmb/docs/grib2/grib2_temp4-0.shtml
   *
   * @return generating process id
   */
  public int getGenProcessId() {
    return GribNumbers.UNDEFINED;
  }

  /**
    Get Background generating process identifier (defined by originating centre)
   @return Background generating process identifier
   */
   public int getBackProcessId() {
     return GribNumbers.UNDEFINED;
  }

  /**
   * Indicator of unit of time range (see Code table 4.4)
   * @return unit of time range
   */
  public abstract int getTimeUnit();

  /**
   * Forecast time in units of getTimeUnit()
   * forecast time for points, should not be used for intervals
   * @return Forecast time
   */
  public int getForecastTime() {
    return GribNumbers.int4(getOctet(19), getOctet(20), getOctet(21), getOctet(22));
  }

  public double getLevelValue1() {
    return GribNumbers.UNDEFINED;
  }

  public int getLevelScale1() {
    return GribNumbers.UNDEFINED;
  }

  public double getLevelValue2() {
    return GribNumbers.UNDEFINED;
  }

  public int getLevelType1() {
    return GribNumbers.UNDEFINED;
  }

  public int getLevelType2() {
    return GribNumbers.UNDEFINED;
  }

  public int getLevelScale2() {
    return GribNumbers.UNDEFINED;
  }

  public boolean isAerosol() {
    return (template == 48);
  }

  public boolean isEnsemble() {
    return false;
  }

  public boolean isEnsembleDerived() {
    return false;
  }

  public boolean isProbability() {
    return false;
  }

  public boolean isTimeInterval() {
    return (template >= 8) && (template <= 14);
  }

  public boolean isSpatialInterval() {
    return (template == 15);
  }

  /* public int getPerturbationNumber() {
    return GribNumbers.UNDEFINED;
  }

  public int getPerturbationType() {
    return GribNumbers.UNDEFINED;
  }

  public boolean isEnsembleDerived() {
    return false;
  }

  public int getNumberEnsembleForecasts() {
    return GribNumbers.UNDEFINED;
  }   */

  /* public double getProbabilityLowerLimit() {
    return GribNumbers.UNDEFINED;
  }

  public double getProbabilityUpperLimit() {
    return GribNumbers.UNDEFINED;
  }

  public int getProbabilityType() {
    return GribNumbers.UNDEFINED;
  }

  public boolean isPercentile() {
    return false;
  }

  public int getPercentileValue() {
    return -1;
  } */

  public void show(Formatter f) {
    f.format("Grib2Pds{ id=%d-%d template=%d, forecastTime= %d timeUnit=%s vertLevel=%f}", getParameterCategory(), getParameterNumber(),
            template, getForecastTime(), getTimeUnit(), getLevelValue1());
  }

  /**
   * Get the index'th byte in the PDS as an unsigned integer.
   * THIS IS ONE BASED (not zero) to correspond with the manual.
   * Will throw Exception if index is out of bounds.
   *
   * @param index 1 based index
   * @return input[index-1] & 0xff
   */
  public final int getOctet(int index) {
    return Byte.toUnsignedInt(input[index - 1]);
  }

  public final int getInt4StartingAtOctet(int index) {
    return GribNumbers.int4(getOctet(index), getOctet(index+1), getOctet(index+2), getOctet(index+3));
  }

  public final int getOctetSigned(int index) {
    return GribNumbers.convertSignedByte(input[index - 1]);
  }

  public final int getRawLength() {
    return input.length;
  }

  protected double getScaledValue(int start) {
    int scale = getOctetSigned(start++);
    int value = GribNumbers.int4(getOctet(start++), getOctet(start++), getOctet(start++), getOctet(start++));
    return applyScaleFactor(scale, value);
  }

  public int getStatisticalProcessType() {
    if (!(this instanceof PdsInterval)) return -1;
    PdsInterval pint = (PdsInterval) this;
    TimeInterval[] ti = pint.getTimeIntervals();
    if (ti.length > 0) {
      TimeInterval ti0 = ti[0];
      return ti0.statProcessType;
    }
    return -1;
  }

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

  public interface PdsAerosol {
    int getAerosolType() ;
    double getAerosolIntervalSizeType();
    double getAerosolSize1();
    double getAerosolSize2();
    double getAerosolIntervalWavelengthType();
    double getAerosolWavelength1();
    double getAerosolWavelength2();
  }

  public interface PdsInterval {
    int getStatisticalProcessType();
    CalendarDate getIntervalTimeEnd();
    int getForecastTime();
    int getNumberTimeRanges();
    int getNumberMissing();
    TimeInterval[] getTimeIntervals();
    long getIntervalHash();
  }

  public interface PdsEnsemble {
    int getPerturbationType();
    int getPerturbationNumber();
    int getNumberEnsembleForecasts();
  }

  public interface PdsEnsembleDerived {
    int getDerivedForecastType();
    int getNumberEnsembleForecasts();
  }

  public interface PdsPercentile {
    int getPercentileValue();
  }

  public interface PdsSpatialInterval {
    int getSpatialStatisticalProcessType();
    int getSpatialProcessType();
    int getNSpatialDataPoints();
  }

  public interface PdsProbability {
    int getForecastProbabilityNumber();
    int getNumberForecastProbabilities();
    int getProbabilityType();
    double getProbabilityLowerLimit();
    double getProbabilityUpperLimit();
    int getProbabilityHashcode();
    String getProbabilityName();
  }

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

  /**
   * Product definition template 4.0 - analysis or forecast at a horizontal level or in a horizontal layer at a point in time
   * Many other templates (1-14) have same fields in sameplaces, so can use this as the superclass.
   */
  private static class Grib2Pds0 extends Grib2Pds {

    Grib2Pds0(byte[] input) {
      super(input);
    }

    @Override
    public int getGenProcessType() {
      return getOctet(12);
    }

    /**
     * Background generating process identifier (defined by originating centre)
     *
     * @return Background generating process id
     */
    @Override
    public int getBackProcessId() {
      return getOctet(13);
    }

    @Override
    public int getGenProcessId() {
      return getOctet(14);
    }

    /**
     * Hours after reference time of data cutoff
     *
     * @return HoursAfter
     */
    public int getHoursAfterCutoff() {
      return GribNumbers.int2(getOctet(15), getOctet(16));
    }

    /**
     * Minutes after reference time of data cutoff
     *
     * @return MinutesAfter
     */
    public int getMinutesAfterCutoff() {
      return getOctet(17);
    }

    /**
     * Indicator of unit of time range (see Code table 4.4)
     *
     * @return TimeRangeUnit
     */
    @Override
    public int getTimeUnit() {
      return getOctet(18);
    }

    /**
     * Type of first fixed surface (see Code table 4.5)
     *
     * @return Type of first fixed surface
     */
    public int getLevelType1() {
      return getOctet(23);
    }

    /**
     * Value of first fixed surface, with scale factor already applied
     *
     * @return float FirstFixedSurfaceValue
     */
    public double getLevelValue1() {
      return getScaledValue(24);
    }

    // debug
    public int getLevelScale() {
      return getOctet(24);
    }

    /**
     * Type of second fixed surface (see Code table 4.5)
     *
     * @return Type of second fixed surface
     */
    public int getLevelType2() {
      return getOctet(29);
    }

    /**
     * Value of second fixed surface, with scale factor already applied
     *
     * @return float FirstFixedSurfaceValue
     */
    public double getLevelValue2() {
      return getScaledValue(30);
    }

    /**
     * length of template (next byte starts info after template, if any
     *
     * @return length of template
     */
    public int templateLength() {
      return 34;
    }

  }

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

  /**
   * Product definition template 4.15 - average, accumulation, extreme values, or other statistically-processed
   * values over a spatial area at a horizontal level or in a horizontal layer at a point in time
   */
  private static class Grib2Pds15 extends Grib2Pds0 implements PdsSpatialInterval {
    Grib2Pds15(byte[] input) {
      super(input);
    }

    // table 4.10
    public int getSpatialStatisticalProcessType() {
      return getOctet(35);
    }

    // code 4.15
    public int getSpatialProcessType() {
      return getOctet(36);
    }

    public int getNSpatialDataPoints() {
      return getOctet(37);
    }

  }

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

  /**
   * Product definition template 4.1 -
   * individual ensemble forecast, control and perturbed, at a horizontal level or in a horizontal layer at a point in time
   */
  private static class Grib2Pds1 extends Grib2Pds0 implements PdsEnsemble {
    Grib2Pds1(byte[] input) {
      super(input);
    }

    public boolean isEnsemble() {
      return true;
    }

    /**
     * Type of ensemble forecast (see Code table 4.6)
     *
     * @return Type of ensemble forecast
     */
    public int getPerturbationType() {
      return getOctet(35);
    }

    /**
     * Perturbation Number - which member of the ensemble is this ?
     *
     * @return Perturbation Number
     */
    public int getPerturbationNumber() {
      return getOctet(36);
    }

    /**
     * Number of forecasts in ensemble
     *
     * @return Number of forecasts in ensemble
     */
    public int getNumberEnsembleForecasts() {
      return getOctet(37);
    }

    @Override
    public int templateLength() {
      return 37;
    }

  }

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

  /**
   * Product definition template 4.11 -
   * individual ensemble forecast, control and perturbed, at a horizontal level or in a horizontal layer in a continuous
   * or non-continuous time interval
   */
  private static class Grib2Pds11 extends Grib2Pds1 implements PdsInterval {

    Grib2Pds11(byte[] input) {
      super(input);
    }

    /**
     * End of overall time interval
     *
     * @return End of overall time interval
     */
    public CalendarDate getIntervalTimeEnd() {
      return calcTime(38);
    }

    /**
     * number of time range specifications describing the time intervals used to calculate the statistically-processed field
     *
     * @return number of time range
     */
    public int getNumberTimeRanges() {
      return getOctet(45);
    }

    /**
     * Total number of data values missing in statistical process
     *
     * @return Total number of data values missing in statistical process
     */
    public final int getNumberMissing() {
      return GribNumbers.int4(getOctet(46), getOctet(47), getOctet(48), getOctet(49));
    }

    public TimeInterval[] getTimeIntervals() {
      return readTimeIntervals(getNumberTimeRanges(), 50);
    }

    @Override
    public int templateLength() {
      return 49 + getNumberTimeRanges() * 12;
    }

    public long getIntervalHash() {
      CRC32 crc32 = new CRC32();
      crc32.update(input, 49, getNumberTimeRanges() * 12);
      return crc32.getValue();
    }

     public void show(Formatter f) {
      super.show(f);
      f.format("%n   Grib2Pds8: endInterval=%s%n", getIntervalTimeEnd());
      for (TimeInterval ti : getTimeIntervals()) {
        ti.show(f);
      }
    }
  }

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

  /**
   * Product definition template 4.61 -
   * individual ensemble forecast, control and perturbed, at a horizontal level or in a horizontal layer in a continuous
   * or non-continuous time interval
   */
  private static class Grib2Pds61 extends Grib2Pds1 implements PdsInterval{

    Grib2Pds61(byte[] input) {
      super(input);
    }

    /**
     * Model version date
     *
     * @return Model version date
     */
    public CalendarDate getModelVersionDate() {
      return calcTime(38);
    }

    /**
     * End of overall time interval
     *
     * @return End of overall time interval
     */
    public CalendarDate getIntervalTimeEnd() {
      return calcTime(45);
    }

    /**
     * number of time range specifications describing the time intervals used to calculate the statistically-processed field
     *
     * @return number of time range
     */
    public int getNumberTimeRanges() {
      return getOctet(52);
    }

    /**
     * Total number of data values missing in statistical process
     *
     * @return Total number of data values missing in statistical process
     */
    public final int getNumberMissing() {
      return GribNumbers.int4(getOctet(53), getOctet(54), getOctet(55), getOctet(56));
    }

    public TimeInterval[] getTimeIntervals() {
      return readTimeIntervals(getNumberTimeRanges(), 57);
    }

    @Override
    public int templateLength() {
      return 56 + getNumberTimeRanges() * 12;
    }

    public long getIntervalHash() {
      CRC32 crc32 = new CRC32();
      crc32.update(input, 56, getNumberTimeRanges() * 12);
      return crc32.getValue();
    }

     public void show(Formatter f) {
      super.show(f);
      f.format("%n   Grib2Pds8: endInterval=%s%n", getIntervalTimeEnd());
      for (TimeInterval ti : getTimeIntervals()) {
        ti.show(f);
      }
    }
  }

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

  /**
   * Product definition template 4.2 -
   * derived forecasts based on all ensemble members at a horizontal level or in a horizontal layer at a point in time
   */
  private static class Grib2Pds2 extends Grib2Pds0 implements PdsEnsembleDerived {

    Grib2Pds2(byte[] input) {
      super(input);
    }

    public boolean isEnsembleDerived() {
      return true;
    }

    /**
     * Derived forecast Type (see Code table 4.7)
     *
     * @return Derived forecast Type
     */
    public int getDerivedForecastType() {
      return getOctet(35);
    }

    /**
     * Number of forecasts in ensemble
     *
     * @return Number of forecasts in ensemble
     */
    public int getNumberEnsembleForecasts() {
      return getOctet(36);
    }

    @Override
    public int templateLength() {
      return 36;
    }

  }

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

  /**
   * Product definition template 4.12 -
   * derived forecasts based on all ensemble members at a horizontal level or in a horizontal layer in a
   * continuous or non-continuous time interval
   */
  private static class Grib2Pds12 extends Grib2Pds2 implements PdsInterval {
    //CalendarDate endInterval; // Date msecs
    //int ft;

    Grib2Pds12(byte[] input) {
      super(input);
      //endInterval = calcTime(37);
      //ft = makeForecastTime(endInterval, getTimeUnit());
    }

    /**
     * End of overall time interval
     *
     * @return End of overall time interval
     */
    public CalendarDate getIntervalTimeEnd() {
      return calcTime(37);
    }

    /**
     * number of time range specifications describing the time intervals used to calculate the statistically-processed field
     *
     * @return number of time range
     */
    public int getNumberTimeRanges() {
      return getOctet(44);
    }

    /**
     * Total number of data values missing in statistical process
     *
     * @return Total number of data values missing in statistical process
     */
    public final int getNumberMissing() {
      return GribNumbers.int4(getOctet(45), getOctet(46), getOctet(47), getOctet(48));
    }

    public TimeInterval[] getTimeIntervals() {
      return readTimeIntervals(getNumberTimeRanges(), 49);
    }

    @Override
    public int templateLength() {
      return 48 + getNumberTimeRanges() * 12;
    }

    public long getIntervalHash() {
      CRC32 crc32 = new CRC32();
      crc32.update(input, 48, getNumberTimeRanges() * 12);
      return crc32.getValue();
    }

    public void show(Formatter f) {
      super.show(f);
      f.format("%n   Grib2Pds8: endInterval=%s%n", getIntervalTimeEnd());
      for (TimeInterval ti : getTimeIntervals()) {
        ti.show(f);
      }
    }
  }

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

  /**
   * Product definition template 4.5 – probability forecasts at a horizontal level or in a horizontal layer at a point in time
   Octet No. Contents
   10 Parameter category (see Code table 4.1)
   11 Parameter number (see Code table 4.2)
   12 Type of generating process (see Code table 4.3)
   13 Background generating process identifier (defined by originating centre)
   14 Forecast generating process identifier (defined by originating centre)
   15–16 Hours after reference time of data cut-off (see Note)
   17 Minutes after reference time of data cut-off
   18 Indicator of unit of time range (see Code table 4.4)
   19–22 Forecast time in units defined by octet 18
   23 Type of first fixed surface (see Code table 4.5)
   24 Scale factor of first fixed surface
   25–28 Scaled value of first fixed surface
   29 Type of second fixed surface (see Code table 4.5)
   30 Scale factor of second fixed surface
   31–34 Scaled value of second fixed surface
   35 Forecast probability number
   36 Total number of forecast probabilities
   37 Probability type (see Code table 4.9)
   38 Scale factor of lower limit
   39–42 Scaled value of lower limit
   43 Scale factor of upper limit
   44–47 Scaled value of upper limit
   Note: Hours greater than 65534 will be coded as 65534
   */
  private static class Grib2Pds5 extends Grib2Pds0 implements PdsProbability {

    Grib2Pds5(byte[] input) {
      super(input);
    }

    public boolean isProbability() {
      return true;
    }

    /**
     * Forecast probability number
     *
     * @return Forecast probability number
     */
    public int getForecastProbabilityNumber() {
      return getOctet(35);
    }

    /**
     * Number of forecasts probabilities
     *
     * @return Number of forecasts probabilities
     */
    public int getNumberForecastProbabilities() {
      return getOctet(36);
    }

    /**
     * Probability type (see Code table 4.9)
     *
     * @return Probability type
     */
    public int getProbabilityType() {
      return getOctet(37);
    }

    public double getProbabilityLowerLimit() {
      int scale = getOctetSigned(38);
      int value = GribNumbers.int4(getOctet(39), getOctet(40), getOctet(41), getOctet(42));
      return applyScaleFactor(scale, value);
    }

    public double getProbabilityUpperLimit() {
      int scale = getOctetSigned(43);
      int value = GribNumbers.int4(getOctet(44), getOctet(45), getOctet(46), getOctet(47));
      return applyScaleFactor(scale, value);
    }

    @Override
    public int getProbabilityHashcode() {
      if (probHash == 0) {
        int result = 0;
        long temp;
        double prob1, prob2;

        switch (getProbabilityType()) {
          case 0:
          case 3:
            prob1 = getProbabilityLowerLimit();
            temp = prob1 != +0.0d ? Double.doubleToLongBits(prob1) : 0L;
            result = (int) (temp ^ (temp >>> 32));
            break;

          case 1:
          case 4:
            prob2 = getProbabilityUpperLimit();
            temp = prob2 != +0.0d ? Double.doubleToLongBits(prob2) : 0L;
            result = (int) (temp ^ (temp >>> 32));
            break;

          case 2:
            prob1 = getProbabilityLowerLimit();
            prob2 = getProbabilityUpperLimit();
            temp = prob1 != +0.0d ? Double.doubleToLongBits(prob1) : 0L;
            result = (int) (temp ^ (temp >>> 32));
            temp = prob2 != +0.0d ? Double.doubleToLongBits(prob2) : 0L;
            result = 31 * result + (int) (temp ^ (temp >>> 32));
            break;

          default:
        }
        result = 31 * result + getProbabilityType();
        probHash = result;
      }
      return probHash;
    }
    int probHash = 0;

        /*
    Code Table Code table 4.9 - Probability type (4.9)
        0: Probability of event below lower limit
        1: Probability of event above upper limit
        2: Probability of event between lower and upper limits (the range includes the lower limit but not the upper limit)
        3: Probability of event above lower limit
        4: Probability of event below upper limit
       -1: Reserved
       -1: Reserved for local use
      255: Missing
     */
    @Override
    public String getProbabilityName() {
      String result;
      try (Formatter f = new Formatter()) {
        int scale1 = Math.max(1, getOctet(38));
        int scale2 = Math.max(1, getOctet(43));

        switch (getProbabilityType()) {
          case 0:
            f.format("below_%s", Format.dfrac(getProbabilityLowerLimit(), scale1));
            break;
          case 1:
            f.format("above_%s", Format.dfrac(getProbabilityUpperLimit(), scale2));
            break;
          case 2:
            if (getProbabilityLowerLimit() == getProbabilityUpperLimit()) {
              f.format("equals_%s", Format.dfrac(getProbabilityLowerLimit(), scale1));
            } else {
              f.format("between_%s_and_%s", Format.dfrac(getProbabilityLowerLimit(), scale1),
                  Format.dfrac(getProbabilityUpperLimit(), scale2));
            }
            break;
          case 3:
            f.format("above_%s", Format.dfrac(getProbabilityLowerLimit(), scale1));
            break;
          case 4:
            f.format("below_%s", Format.dfrac(getProbabilityUpperLimit(), scale2));
            break;
          default:
            f.format("UknownProbType=%d", getProbabilityType());
        }
        result = StringUtil2.removeFromEnd(f.toString(), '0');
      }
      return StringUtil2.removeFromEnd(result, '.');
    }

    @Override
    public int templateLength() {
      return 47;
    }
  }

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

  /**
   * Product definition template 4.9 – probability forecasts at a horizontal level or in a horizontal layer in a continuous or non-continuous time interval
   Octet No. Contents
   10 Parameter category (see Code table 4.1)
   11 Parameter number (see Code table 4.2)
   12 Type of generating process (see Code table 4.3)
   13 Background generating process identifier (defined by originating centre)
   14 Forecast generating process identifier (defined by originating centre)
   15–16 Hours after reference time of data cut-off (see Note 1)
   17 Minutes after reference time of data cut-off
   18 Indicator of unit of time range (see Code table 4.4)
   19–22 Forecast time in units defined by octet 18 (see Note 2)
   23 Type of first fixed surface (see Code table 4.5)
   24 Scale factor of first fixed surface
   25–28 Scaled value of first fixed surface
   29 Type of second fixed surface (see Code table 4.5)
   30 Scale factor of second fixed surface
   31–34 Scaled value of second fixed surface
   35 Forecast probability number
   36 Total number of forecast probabilities
   37 Probability type (see Code table 4.9)
   38 Scale factor of lower limit
   39–42 Scaled value of lower limit
   43 Scale factor of upper limit
   44–47 Scaled value of upper limit
   48–49 Year of end of overall time interval
   50 Month of end of overall time interval
   51 Day of end of overall time interval
   52 Hour of end of overall time interval
   53 Minute of end of overall time interval
   54 Second of end of overall time interval
   55 n – number of time range specifications describing the time intervals used to calculate the statistically processed field
   56–59 Total number of data values missing in the statistical process
   60–71 Specification of the outermost (or only) time range over which statistical processing is done
   60 Statistical process used to calculate the processed field from the field at each time increment during the time range (see Code table 4.10)
   61 Type of time increment between successive fields used in the statistical processing (see Code table 4.11)
   62 Indicator of unit of time for time range over which statistical processing is done (see Code table 4.4)
   63–66 Length of the time range over which statistical processing is done, in units defined by the previous octet
   67 Indicator of unit of time for the increment between the successive fields used (see Code table 4.4)
   68–71 Time increment between successive fields, in units defined by the previous octet (see Note 3)
   72–nn These octets are included only if n > 1, where nn = 59 + 12 x n
   72–83 As octets 60 to 71, next innermost step of processing
   84–nn Additional time range specifications, included in accordance with the value of n. Contents
   as octets 60 to 71, repeated as necessary.
   Notes:
   (1) Hours greater than 65534 will be coded as 65534.
   (2) The reference time in section 1 and the forecast time together define the beginning of the overall time interval.
   (3) An increment of zero means that the statistical processing is the result of a continuous (or near continuous) process, not
   the processing of a number of discrete samples. Examples of such continuous processes are the temperatures measured
   by analogue maximum and minimum thermometers or thermographs, and the rainfall measured by a raingauge.
   The reference and forecast times are successively set to their initial values plus or minus the increment, as defined by
   the type of time increment (one of octets 46, 58, 70, ...). For all but the innermost (last) time range, the next inner range is
   then processed using these reference and forecast times as the initial reference and forecast times.
   */
  private static class Grib2Pds9 extends Grib2Pds5 implements PdsInterval {
    // CalendarDate endInterval; // Date msecs
    //int ft;

    Grib2Pds9(byte[] input) {
      super(input);
      //endInterval = calcTime(48);
      //ft = makeForecastTime(endInterval, getTimeUnit());
    }

    /**
     * End of overall time interval
     *
     * @return End of overall time interval
     */
    public CalendarDate getIntervalTimeEnd() {
      return calcTime(48);
    }

    /* @Override
    public int getForecastTime() {
      return ft;
    } */

    /**
     * number of time range specifications describing the time intervals used to calculate the statistically-processed field
     *
     * @return number of time range
     */
    public int getNumberTimeRanges() {
      return getOctet(55);
    }

    /**
     * Total number of data values missing in statistical process
     *
     * @return Total number of data values missing in statistical process
     */
    public final int getNumberMissing() {
      return GribNumbers.int4(getOctet(56), getOctet(57), getOctet(58), getOctet(59));
    }

    public TimeInterval[] getTimeIntervals() {
      return readTimeIntervals(getNumberTimeRanges(), 60);
    }

    @Override
    public int templateLength() {
      return 59 + getNumberTimeRanges() * 12;
    }

    public long getIntervalHash() {
      CRC32 crc32 = new CRC32();
      crc32.update(input, 59, getNumberTimeRanges() * 12);
      return crc32.getValue();
    }

    public void show(Formatter f) {
      super.show(f);
      f.format("%n   Grib2Pds9: endInterval=%s%n", getIntervalTimeEnd());
      for (TimeInterval ti : getTimeIntervals()) {
        ti.show(f);
      }
    }

  }

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

  /**
   * Product definition template 4.8 -
   * Average, accumulation, extreme values or other statistically processed values at a horizontal level or in a
   * horizontal layer in a continuous or non-continuous time interval
   */
  private static class Grib2Pds8 extends Grib2Pds0 implements PdsInterval {
    // CalendarDate endInterval; // Date msecs

    Grib2Pds8(byte[] input) {
      super(input);
      //endInterval = calcTime(35);
    }

    /**
     * End of overall time interval
     *
     * @return End of overall time interval
     */
    @Override
    public CalendarDate getIntervalTimeEnd() {
      return calcTime(35);
    }

    /**
     * number of time range specifications describing the time intervals used to calculate the statistically-processed field
     *
     * @return number of time range
     */
    @Override
    public int getNumberTimeRanges() {
      return getOctet(42);
    }

    /**
     * Total number of data values missing in statistical process
     *
     * @return Total number of data values missing in statistical process
     */
    @Override
    public final int getNumberMissing() {
      return GribNumbers.int4(getOctet(43), getOctet(44), getOctet(45), getOctet(46));
    }

    @Override
    public TimeInterval[] getTimeIntervals() {
      return readTimeIntervals(getNumberTimeRanges(), 47);
    }

    @Override
    public int templateLength() {
      return 46 + getNumberTimeRanges() * 12;
    }

    public void show(Formatter f) {
      super.show(f);
      try {
        f.format("%n   Grib2Pds8: endInterval=%s%n", getIntervalTimeEnd());
        for (TimeInterval ti : getTimeIntervals()) ti.show(f);
      } catch (Throwable t) {
        f.format("%n   Grib2Pds8: endInterval error=%s%n", t.getMessage());
      }
    }

    public long getIntervalHash() {
      CRC32 crc32 = new CRC32();
      crc32.update(input, 46, 12);
      if (getNumberTimeRanges() > 1)
        crc32.update(input, 58, 1);
      return crc32.getValue();
    }

  }

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

  /**
   * Product definition template 4.6 -
   * percentile forecasts at a horizontal level or in a horizontal layer at a point in time
   */
  private static class Grib2Pds6 extends Grib2Pds0 implements PdsPercentile {

    Grib2Pds6(byte[] input) {
      super(input);
    }

    public boolean isPercentile() {
      return true;
    }

    /**
     * Percentile - from 100 to 0
     *
     * @return Percentile
     */
    public int getPercentileValue() {
      return getOctet(35);
    }

    @Override
    public int templateLength() {
      return 36;
    }

  }

  /**
   * Product definition template 4.10 -
   * percentile forecasts at a horizontal level or in a horizontal layer in a continuous or non-continuous time interval
   */
  private static class Grib2Pds10 extends Grib2Pds6 implements PdsInterval {
    // CalendarDate endInterval;

    Grib2Pds10(byte[] input) {
      super(input);
      //endInterval = calcTime(36);
    }

    /**
     * End of overall time interval
     *
     * @return End of overall time interval
     */
    public CalendarDate getIntervalTimeEnd() {
      return calcTime(36);
    }

    /**
     * number of time range specifications describing the time intervals used to calculate the statistically-processed field
     *
     * @return number of time range
     */
    public int getNumberTimeRanges() {
      return getOctet(43);
    }

    /**
     * Total number of data values missing in statistical process
     *
     * @return Total number of data values missing in statistical process
     */
    public final int getNumberMissing() {
      return GribNumbers.int4(getOctet(44), getOctet(45), getOctet(46), getOctet(47));
    }

    public TimeInterval[] getTimeIntervals() {
      return readTimeIntervals(getNumberTimeRanges(), 48);
    }

    @Override
    public int templateLength() {
      return 48 + getNumberTimeRanges() * 12;
    }

    public long getIntervalHash() {
      CRC32 crc32 = new CRC32();
      crc32.update(input, 48, getNumberTimeRanges() * 12);
      return crc32.getValue();
    }

  }

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

  /**
   * Product definition template 4.30 - satellite product
   *
   * @deprecated  4.31 should be used
   */
  private static class Grib2Pds30 extends Grib2Pds {

    Grib2Pds30(byte[] input) {
      super(input);
      log.debug("Product Definition Template 4.30 is deprecated in favor of 4.31 (WMO Manual on Codes");
    }

    // LOOK - could put this into a dummy superclass in case others need

    @Override
    public int getTimeUnit() {
      return 0;
    }

    @Override
    public int getForecastTime() {
      return 0;
    }

    /**
     * Observation generating process identifier (defined by originating centre)
     *
     * @return GenProcess
     */
    public int getGenProcessId() {
      return getOctet(13);
    }

    /**
     * Number of contributing spectral bands (NB)
     *
     * @return Number of contributing spectral
     */
    public int getNumSatelliteBands() {
      return getOctet(14);
    }

    /**
     * SatelliteBand
     *
     * @return SatelliteBands
     */
    public SatelliteBand[] getSatelliteBands() {
      int nb = getNumSatelliteBands();
      SatelliteBand[] result = new SatelliteBand[nb];
      int pos = 15;
      for (int i = 0; i < nb; i++) {
        SatelliteBand sb = new SatelliteBand();
        sb.number = GribNumbers.int2(getOctet(pos), getOctet(pos + 1));
        sb.series = GribNumbers.int2(getOctet(pos + 2), getOctet(pos + 3));
        sb.instrumentType = getOctet(pos + 4);
        int scaleFactor = getOctetSigned(pos + 5);
        int svalue = GribNumbers.int4(getOctet(pos + 6), getOctet(pos + 7), getOctet(pos + 8), getOctet(pos + 9));
        sb.value = applyScaleFactor(scaleFactor, svalue);
        pos += 10;
        result[i] = sb;
      }
      return result;
    }

    public int templateLength() {
      return 14 + getNumSatelliteBands() * 10;
    }
  }

  public static class SatelliteBand {
    public int series; // Satellite series of band nb (code table defined by originating/generating centre)
    public int number; // Satellite numbers of band nb (code table defined by originating/generating centre)
    public int instrumentType; // Instrument types of band nb (code table defined by originating/generating centre)
    public double value; // value of central wave number of band nb (units: m**-1)
  }

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

    /**
     * Product definition template 4.31 - satellite product
     */
    private static class Grib2Pds31 extends Grib2Pds {

        static final int octetsPerBand = 11;

        Grib2Pds31(byte[] input) {
            super(input);
        }

        // LOOK - could put this into a dummy superclass in case others need

        @Override
        public int getTimeUnit() {
            return 0;
        }

        @Override
        public int getForecastTime() {
            return 0;
        }

        /**
         * Observation generating process identifier (defined by originating centre)
         *
         * @return GenProcess
         */
        public int getGenProcessId() {
            return getOctet(13);
        }

        /**
         * Number of contributing spectral bands (NB)
         *
         * @return Number of contributing spectral
         */
        public int getNumSatelliteBands() {
            return getOctet(14);
        }

        /**
         * SatelliteBand
         *
         * @return SatelliteBands
         */
        public SatelliteBand[] getSatelliteBands() {
            int nb = getNumSatelliteBands();
            SatelliteBand[] result = new SatelliteBand[nb];
            int pos = 15;
            for (int i = 0; i < nb; i++) {
                SatelliteBand sb = new SatelliteBand();
                sb.number = GribNumbers.int2(getOctet(pos), getOctet(pos + 1));
                sb.series = GribNumbers.int2(getOctet(pos + 2), getOctet(pos + 3));
                sb.instrumentType = GribNumbers.int2(getOctet(pos + 4), getOctet(pos + 5));
                int scaleFactor = getOctetSigned(pos + 6);
                int svalue = GribNumbers.int4(getOctet(pos + 7), getOctet(pos + 8), getOctet(pos + 9), getOctet(pos +  10));
                sb.value = applyScaleFactor(scaleFactor, svalue);
                pos += octetsPerBand;
                result[i] = sb;
            }
            return result;
        }

        public int templateLength() {
            return 14 + getNumSatelliteBands() * octetsPerBand;
        }
    }

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

  /* Product definition template 4.48 – analysis or forecast at a horizontal level or in a horizontal layer at a point in time for optical properties of aerosol
  Octet No. Contents
  10 Parameter category (see Code table 4.1)
  11 Parameter number (see Code table 4.2)
  12–13 Aerosol type (see Common Code table C–14)
  14 Type of interval for first and second size (see Code table 4.91)
  15 Scale factor of first size
  16–19 Scaled value of first size in metres
  20 Scale factor of second size
  21–24 Scaled value of second size in metres
  25 Type of interval for first and second wavelength (see Code table 4.91)
  26 Scale factor of first wavelength
  27–30 Scaled value of first wavelength in metres
  31 Scale factor of second wavelength
  32–35 Scaled value of second wavelength in metres
  36 Type of generating process (see Code table 4.3)
  37 Background generating process identifier (defined by originating centre)
  38 Analysis or forecast generating process identifier (defined by originating centre)
  39–40 Hours of observational data cut-off after reference time (see Note)
  41 Minutes of observational data cut-off after reference time
  42 Indicator of unit of time range (see Code table 4.4)
  43–46 Forecast time in units defined by octet 42
  47 Type of first fixed surface (see Code table 4.5)
  48 Scale factor of first fixed surface
  49–52 Scaled value of first fixed surface
  53 Type of second fixed surface (see Code table 4.5)
  54 Scale factor of second fixed surface
  55–58 Scaled value of second fixed surface
  Note: Hours greater than 65534 will be coded as 65534.
  */

  private static class Grib2Pds48 extends Grib2Pds implements PdsAerosol {

    Grib2Pds48(byte[] input) {
      super(input);
    }

    //   12–13 Aerosol type (see Common Code table C–14)
    public int getAerosolType() {
      return GribNumbers.uint2(getOctet(12), getOctet(13));
    }

    //  14 Type of interval for first and second size (see Code table 4.91)
    public double getAerosolIntervalSizeType() {
      return getOctet(14);
    }

    //  15 Scale factor of first size
    //  16–19 Scaled value of first size in metres
    public double getAerosolSize1() {
      return getScaledValue(15);
    }

    //  20 Scale factor of second size
    //  21–24 Scaled value of second size in metres
    public double getAerosolSize2() {
      return getScaledValue(20);
    }

    //  25 Type of interval for first and second wavelength (see Code table 4.91)
    public double getAerosolIntervalWavelengthType() {
      return getOctet(25);
    }

    //  26 Scale factor of first wavelength
    //  27–30 Scaled value of first wavelength in metres
    public double getAerosolWavelength1() {
      return getScaledValue(26);
    }

    //  31 Scale factor of second wavelength
    //  32–35 Scaled value of second wavelength in metres
    public double getAerosolWavelength2() {
      return getScaledValue(31);
    }

    //  36 Type of generating process (see Code table 4.3)
    @Override
    public int getGenProcessType() {
      return getOctet(36);
    }

    //  37 Background generating process identifier (defined by originating centre)
    @Override
    public int getBackProcessId() {
      return getOctet(37);
    }

    //  38 Analysis or forecast generating process identifier (defined by originating centre)
    @Override
    public int getGenProcessId() {
      return getOctet(38);
    }

    //  39–40 Hours of observational data cut-off after reference time (see Note)
    //  Note: Hours greater than 65534 will be coded as 65534.
    public int getHoursAfterCutoff() {
      return GribNumbers.int2(getOctet(39), getOctet(40));
    }

    //  41 Minutes of observational data cut-off after reference time
    public int getMinutesAfterCutoff() {
      return getOctet(41);
    }

    //  42 Indicator of unit of time range (see Code table 4.4)
    @Override
    public int getTimeUnit() {
      return getOctet(42);
    }

    //  43–46 Forecast time in units defined by octet 42
    public int getForecastTime() {
      return GribNumbers.int4(getOctet(43), getOctet(44), getOctet(45), getOctet(46));
    }

    //  47 Type of first fixed surface (see Code table 4.5)
    @Override
    public int getLevelType1() {
      return getOctet(47);
    }

    //  48 Scale factor of first fixed surface
    @Override
    public int getLevelScale1() {
      return getOctet(48);
    }

    //  49–52 Scaled value of first fixed surface
    @Override
    public double getLevelValue1() {
      return getScaledValue(48);
    }

    //  53 Type of second fixed surface (see Code table 4.5)
    @Override
    public int getLevelType2() {
      return getOctet(53);
    }

    //  54 Scale factor of second fixed surface
    @Override
    public int getLevelScale2() {
      return getOctet(54);
    }

     // 55–58 Scaled value of second fixed surface
     @Override
     public double getLevelValue2() {
       return getScaledValue(54);
     }

    public int templateLength() {
      return 58;
    }
  }

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

  // translate 7 byte time into CalendarDate
  // null means use refTime
  protected CalendarDate calcTime(int startIndex) {

    int year = GribNumbers.int2(getOctet(startIndex++), getOctet(startIndex++));
    int month = getOctet(startIndex++);
    int day = getOctet(startIndex++);
    int hour = getOctet(startIndex++);
    int minute = getOctet(startIndex++);
    int second = getOctet(startIndex++);

    if ((year == 0) && (month == 0) && (day == 0) && (hour == 0) && (minute == 0) && (second == 0))
      return CalendarDate.UNKNOWN;

   // href.t00z.prob.f36.grib2
     if (hour > 23) {
       day += (hour/24);
       hour = hour % 24;
     }

    return CalendarDate.of(null, year, month, day, hour, minute, second);
  }

  /**
   * Apply scale factor to value, return a double result.
   *
   * @param scale signed scale factor
   * @param value apply to this value
   * @return   value ^ -scale
   */
  double applyScaleFactor(int scale, int value) {
    return ((scale == 0) || (scale == 255) || (value == 0)) ? value : value * Math.pow(10, -scale);
  }

  TimeInterval[] readTimeIntervals(int n, int startIndex) {
    TimeInterval[] result = new TimeInterval[n];
    for (int i = 0; i < n; i++) {
      TimeInterval ti = new TimeInterval();
      ti.statProcessType = getOctet(startIndex++);
      ti.timeIncrementType = getOctet(startIndex++);
      ti.timeRangeUnit = getOctet(startIndex++);
      ti.timeRangeLength = GribNumbers.int4(getOctet(startIndex++), getOctet(startIndex++), getOctet(startIndex++), getOctet(startIndex++));
      ti.timeIncrementUnit = getOctet(startIndex++);
      ti.timeIncrement = GribNumbers.int4(getOctet(startIndex++), getOctet(startIndex++), getOctet(startIndex++), getOctet(startIndex++));
      result[i] = ti;
    }
    return result;
  }

  public static class TimeInterval {
    public int statProcessType; // (code table 4.10) Statistical process used to calculate the processed field from the field at each time increment during the time range
    public int timeIncrementType;  // (code table 4.11) Type of time increment between successive fields used in the statistical processing<
    public int timeRangeUnit;  // (code table 4.4) Indicator of unit of time for time range over which statistical processing is done
    public int timeRangeLength; // Length of the time range over which statistical processing is done, in units defined by the previous octet
    public int timeIncrementUnit; // (code table 4.4) Indicator of unit of time for the increment between the successive fields used
    public int timeIncrement; // Time increment between successive fields, in units defined by the previous octet

    public void show(Formatter f) {
      f.format("  TimeInterval: statProcessType= %d, timeIncrementType= %d, timeRangeUnit= %d, timeRangeLength= %d, timeIncrementUnit= %d, timeIncrement=%d%n",
              statProcessType, timeIncrementType, timeRangeUnit, timeRangeLength, timeIncrementUnit, timeIncrement);
    }
  }
}

/*

Hi John,

I did create the degrib program, but I'm fairly positive you're using your own software and are just wondering about how to interpret NDFD's encoding of the valid time.  The correct people to answer that would be the people who encoded the messages which is why I've included nws.ndfd@noaa.gov.  There probably should be a FAQ on this subject.

As you point out, the reference time (Ref) is easy (The reference time is 2010-09-21T12:00:00Z)
The forecast time (ForeT) can similarly be read (Forecast time in units defined by previous octet == 6) (Presumably the earlier units was hours)
The time range (range) also can be read (51: Length of the time range over which statistical processing is done, in units defined by the previous octet == 6)

So now the question is what does one do with these?  We know we have a 6 hour interval which starts or stops around Ref + ForeT.  So we could either have:
A) End of Interval = Ref + ForeT + range; Begin of Interval = Ref + ForeT
or
B) End of Interval = Ref + ForeT; Begin of Interval = Ref  + ForeT - range

The way NDFD decided to encode it is with B).  The reasoning was that the valid times are always the end of the period, so B made more sense to the folks encoding the NDFD.  Also, the logic went, if decoders were confused, then the "end of overall time interval" would clarify whether A) or B) was chosen.

So now we go back to the note:
> (2) The reference time in section 1 and the forecast time together
>      define the beginning of the overall time interval.
and try to determine whether the people encoding the message were in error.  It specifies that the two values together "define" the beginning, but it doesn't say that "the sum of those two values equals the beginning of the overall time interval".  If you think that is funky logic, consider the statement that "Pressure and Volume together define Temperature".  Does that mean P + V = T?  No, it meas P*V / nR = T.  So note 2, as written, unfortunately has a lot of wiggle room.

The result is that when I was writing degrib, I couldn't trust either equation A) or B), so I went with a simpler method... I simply read the end of the interval.  If there was a range I used:

End of interval (EI) = (bytes 36-42 show an "end of overall time interval")
C1) End of Interval = EI;      Begin of Interval = EI - range

and if there was no interval then I used:
C2) End of Interval = Begin of Interval = Ref + ForeT.

Using equations C1 and C2 has the elegance of being simple, and not having to convert the word 'define' into an equation.

Does that help?

Arthur
Arthur.Taylor@noaa.gov

 */


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

import com.google.common.escape.Escaper;
import com.google.common.html.HtmlEscapers;
import com.google.common.net.UrlEscapers;
import thredds.client.catalog.*;
import ucar.nc2.units.DateRange;
import ucar.nc2.units.DateType;
import ucar.nc2.units.TimeDuration;
import ucar.unidata.util.Format;

import java.util.ArrayList;
import java.util.Formatter;

/**
 * Create Html from a dataset
 *
 * @author caron
 * @since 1/8/2015
 */
public class DatasetHtmlWriter {
  private Escaper htmlEscaper = HtmlEscapers.htmlEscaper();
  private Escaper urlPathEscaper = UrlEscapers.urlPathSegmentEscaper();
  private Escaper urlParamEscaper = UrlEscapers.urlFormParameterEscaper();

  /**
   * Write an Html representation of the given dataset.
   *
 With datasetEvents, catrefEvents = true, this is used to construct an HTML page on the client
   * (eg using HtmlPage); the client then detects URL clicks and processes.
   *
 With datasetEvents, catrefEvents = false, this is used to construct an HTML page on the server.
   * (eg using HtmlPage); the client then detects URL clicks and processes.
   *
   * @param out           put HTML here.
   * @param ds            the dataset.
   * @param complete      if true, add HTML header and ender so its a complete, valid HTML page.
   * @param isServer      if true, then we are in the thredds data server, so do the following:

   *
 append "html" to DODS Access URLs
   *

   * @param datasetEvents if true, prepend "dataset:" to any dataset access URLS
   * @param catrefEvents  if true, prepend "catref:" to any catref URLS
   */

  public void writeHtmlDescription(Formatter out, Dataset ds,
                                   boolean complete, boolean isServer,
                                   boolean datasetEvents, boolean catrefEvents,
                                   boolean resolveRelativeUrls) {

    if (ds == null) return;

    if (complete) {
      out.format("<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"%n");
      out.format("        \"http://www.w3.org/TR/html4/loose.dtd\">%n");
      out.format("%n");
      out.format("%n");
      out.format("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=iso-8859-1\">%n");
      out.format("%n");
      out.format("%n");
    }

    out.format("
Dataset: %s
%n
", ds.getName());
    if (ds.getDataFormatName() != null)
      out.format("
Data format: %s
%n", htmlEscaper.escape(ds.getDataFormatName()));

    if ((ds.getDataSize() > 0))
      out.format("
Data size: %s
%n", Format.formatByteSize(ds.getDataSize()));

    if (ds.getFeatureTypeName() != null)
      out.format("
Feature type: %s
%n", htmlEscaper.escape(ds.getFeatureTypeName()));

    if (ds.getCollectionType() != null)
      out.format("
Collection type: %s
%n", htmlEscaper.escape(ds.getCollectionType()));

    if (ds.isHarvest())
      out.format("
Harvest: true
%n");

    if (ds.getAuthority() != null)
      out.format("
Naming Authority: %s
%n%n", htmlEscaper.escape(ds.getAuthority()));

    if (ds.getId() != null)
      out.format("
ID: %s
%n", htmlEscaper.escape(ds.getId()));

    if (ds.getRestrictAccess() != null)
      out.format("
RestrictAccess: %s
%n", htmlEscaper.escape(ds.getRestrictAccess()));

    if (ds instanceof CatalogRef) {
      CatalogRef catref = (CatalogRef) ds;
      String href = resolveRelativeUrls | catrefEvents ? resolve(ds, catref.getXlinkHref()) : catref.getXlinkHref();
      if (catrefEvents) href = "catref:" + href;
      out.format("
CatalogRef: %s
%n", makeHref(href, null, null));
    }

    out.format("
%n");

    java.util.List<Documentation> docs = ds.getDocumentation();
    if (docs.size() > 0) {
      out.format("
Documentation:
%n
%n");
      for (Documentation doc : docs) {
        String type = (doc.getType() == null) ? "" : "" + htmlEscaper.escape(doc.getType()) + ": ";
        String inline = doc.getInlineContent();
        if ((inline != null) && (inline.length() > 0))
          out.format("
%s %s
%n", type, htmlEscaper.escape(inline));
        if (doc.hasXlink()) {
          out.format("
%s %s
%n", type, makeHref(doc.getXlinkHref(), null, doc.getXlinkTitle()));
        }
      }
      out.format("
%n");
    }

    java.util.List<Access> access = ds.getAccess();
    if (access.size() > 0) {
      out.format("
Access:
%n
%n");
      for (Access a : access) {
        Service s = a.getService();
        String urlString = resolveRelativeUrls || datasetEvents ? a.getStandardUrlName() : a.getUnresolvedUrlName();
        String queryString = null;
        // String fullUrlString = urlString;
        if (datasetEvents) urlString = "dataset:" + urlString;

        ServiceType stype = s.getType();
        if (isServer && stype != null)
          switch (stype) {
            case OPENDAP:
            case DODS:
              urlString = urlString + ".html";
              break;

            case DAP4:
              urlString = urlString + ".dmr.xml";
              break;

            case WCS:
              queryString = "service=WCS&version=1.0.0&request=GetCapabilities";
              break;

            case WMS:
              queryString = "service=WMS&version=1.3.0&request=GetCapabilities";
              break;

            case NCML:
            case UDDC:
            case ISO:
              String catalogUrl = ds.getCatalogUrl();
              String datasetId = ds.getId();
              if (catalogUrl != null && datasetId != null) {
                if (catalogUrl.indexOf('#') > 0)
                  catalogUrl = catalogUrl.substring(0, catalogUrl.lastIndexOf('#'));
                /* try {
                  catalogUrl = URLEncoder.encode(catalogUrl, "UTF-8");
                  datasetId = URLEncoder.encode(datasetId, "UTF-8");
                } catch (UnsupportedEncodingException e) {
                  e.printStackTrace();
                } */
                queryString = "catalog=" + urlParamEscaper.escape(catalogUrl) + "&dataset=" + urlParamEscaper.escape(datasetId);
              }
              break;

            case NetcdfSubset:
              urlString = urlString + "/dataset.html";
              break;

            case CdmRemote:
              queryString = "req=cdl";
              break;
            case CdmrFeature:
              queryString = "req=form";
          }
        out.format("
 %s: %s
%n", s.getServiceTypeName(), makeHref(urlString, queryString, null));
      }
      out.format("
%n");
    }

    java.util.List<ThreddsMetadata.Contributor> contributors = ds.getContributors();
    if (contributors.size() > 0) {
      out.format("
Contributors:
%n
%n");
      for (ThreddsMetadata.Contributor t : contributors) {
        String role = (t.getRole() == null) ? "" : " (" + htmlEscaper.escape(t.getRole()) + ") ";
        out.format("
%s %s
%n", htmlEscaper.escape(t.getName()), role);
      }
      out.format("
%n");
    }

    java.util.List<ThreddsMetadata.Vocab> keywords = ds.getKeywords();
    if (keywords.size() > 0) {
      out.format("
Keywords:
%n
%n");
      for (ThreddsMetadata.Vocab t : keywords) {
        String vocab = (t.getVocabulary() == null) ? "" : " (" + htmlEscaper.escape(t.getVocabulary()) + ") ";
        out.format("
%s %s
%n", htmlEscaper.escape(t.getText()), vocab);
      }
      out.format("
%n");
    }

    java.util.List<DateType> dates = ds.getDates();
    if (dates.size() > 0) {
      out.format("
Dates:
%n
%n");
      for (DateType d : dates) {
        String type = (d.getType() == null) ? "" : " (" + htmlEscaper.escape(d.getType()) + ") ";
        out.format("
%s %s
%n", htmlEscaper.escape(d.getText()), type);
      }
      out.format("
%n");
    }

    java.util.List<ThreddsMetadata.Vocab> projects = ds.getProjects();
    if (projects.size() > 0) {
      out.format("
Projects:
%n
%n");
      for (ThreddsMetadata.Vocab t : projects) {
        String vocab = (t.getVocabulary() == null) ? "" : " (" + htmlEscaper.escape(t.getVocabulary()) + ") ";
        out.format("
%s %s
%n", htmlEscaper.escape(t.getText()), vocab);
      }
      out.format("
%n");
    }

    java.util.List<ThreddsMetadata.Source> creators = ds.getCreators();
    if (creators.size() > 0) {
      out.format("
Creators:
%n
%n");
      for (ThreddsMetadata.Source t : creators) {
        out.format("
%s
%n", htmlEscaper.escape(t.getName()));
        out.format("
email: %s
%n", htmlEscaper.escape(t.getEmail()));
        if (t.getUrl() != null) {
          String newUrl = resolveRelativeUrls ? makeHrefResolve(ds, t.getUrl(), null) : makeHref(t.getUrl(), null, null);
          out.format("
 %s
%n", newUrl);
        }
        out.format("

%n");
      }
      out.format("
%n");
    }

    java.util.List<ThreddsMetadata.Source> publishers = ds.getPublishers();
    if (publishers.size() > 0) {
      out.format("
Publishers:
%n
%n");
      for (ThreddsMetadata.Source t : publishers) {
        out.format("
%s
%n", htmlEscaper.escape(t.getName()));
        out.format("
email: %s%n", htmlEscaper.escape(t.getEmail()));
        if (t.getUrl() != null) {
          String urlLink = resolveRelativeUrls ? makeHrefResolve(ds, t.getUrl(), null) : makeHref(t.getUrl(), null, null);
          out.format("
 %s
%n", urlLink);
        }
        out.format("
%n");
      }
      out.format("
%n");
    }

     /*
     4.2:

Variables:

Vocabulary [DIF]:

Reflectivity =   = EARTH SCIENCE > Spectral/Engineering > Radar > Radar Reflectivity (db)

Velocity =   = EARTH SCIENCE > Spectral/Engineering > Radar > Doppler Velocity (m/s)

SpectrumWidth =   = EARTH SCIENCE > Spectral/Engineering > Radar > Doppler Spectrum Width (m/s)

     4.3:

Variables:

Vocabulary [CF-1.0]:

d3d (meters)  =  3D Depth at Nodes

         = depth_at_nodes

depth (meters)  =  Bathymetry = depth

eta (m)  =   =

temp (Celsius)  =  Temperature

         = sea_water_temperature

u (m/s)  =  Eastward Water

          Velocity

         = eastward_sea_water_velocity

v (m/s)  =  Northward Water

          Velocity

         = northward_sea_water_velocity

      */
    java.util.List<ThreddsMetadata.VariableGroup> vars = ds.getVariables();
    if (vars.size() > 0) {
      out.format("
Variables:
%n
%n");
      for (ThreddsMetadata.VariableGroup t : vars) {

        out.format("
Vocabulary [");
        if (t.getVocabUri() != null) {
          ThreddsMetadata.UriResolved uri = t.getVocabUri();
          String vocabLink = resolveRelativeUrls ? makeHref(uri.resolved.toString(), null, t.getVocabulary()) : makeHref(uri.href, null, t.getVocabulary());
          out.format(vocabLink);
        } else {
          out.format(htmlEscaper.escape(t.getVocabulary()));
        }
        out.format("]:%n
%n");

        java.util.List<ThreddsMetadata.Variable> vlist = t.getVariableList();
        if (vlist.size() > 0) {
          for (ThreddsMetadata.Variable v : vlist) {
            String units = (v.getUnits() == null || v.getUnits().length() == 0) ? "" : " (" + v.getUnits() + ") ";
            out.format("
%s = ", htmlEscaper.escape(v.getName() + units));
            if (v.getDescription() != null)
              out.format(" %s = ", htmlEscaper.escape(v.getDescription()));
            if (v.getVocabularyName() != null)
              out.format("%s", htmlEscaper.escape(v.getVocabularyName()));
            out.format("%n");
          }
        }
        out.format("
%n");
      }
      out.format("
%n");
    }

    // LOOK what about VariableMapLink string ??
    if (ds.getVariableMapLink() != null) {
      out.format("
Variables:
%n");
      ThreddsMetadata.UriResolved uri = ds.getVariableMapLink();
      out.format("

%s

%n", makeHref(uri.resolved.toASCIIString(), null, "VariableMap"));
    }

    ThreddsMetadata.GeospatialCoverage gc = ds.getGeospatialCoverage();
    if (gc != null) {
      out.format("
GeospatialCoverage:
%n
%n");
      // if (gc.isGlobal()) out.format("
 Global %n");

      out.format("
 Longitude:  %s
%n", rangeString(gc.getEastWestRange()));
      out.format("
 Latitude:  %s
%n", rangeString(gc.getNorthSouthRange()));
      if (gc.getUpDownRange() != null) {
        out.format("
 Altitude:  %s (positive is %s)
%n", rangeString(gc.getUpDownRange()), gc.getZPositive());
      }

      java.util.List<ThreddsMetadata.Vocab> nlist = gc.getNames();
      if ((nlist != null) && (nlist.size() > 0)) {
        out.format("
  Names:
%n");
        for (ThreddsMetadata.Vocab elem : nlist) {
          out.format("
%s
%n", htmlEscaper.escape(elem.getText()));
        }
        out.format("
%n");
      }
      out.format("
%n");
    }

    DateRange tc = ds.getTimeCoverage();
    if (tc != null) {
      out.format("
TimeCoverage:
%n
%n");
      DateType start = tc.getStart();
      if (start != null)
        out.format("
  Start:  %s
%n", start.toString());
      DateType end = tc.getEnd();
      if (end != null) {
        out.format("
  End:  %s
%n", end.toString());
      }
      TimeDuration duration = tc.getDuration();
      if (duration != null)
        out.format("
  Duration:  %s
%n", htmlEscaper.escape(duration.toString()));
      TimeDuration resolution = tc.getResolution();
      if (resolution != null) {
        out.format("
  Resolution:  %s
%n", htmlEscaper.escape(resolution.toString()));
      }
      out.format("
%n");
    }

    java.util.List<ThreddsMetadata.MetadataOther> metadata = ds.getMetadataOther();
    boolean gotSomeMetadata = false;
    for (ThreddsMetadata.MetadataOther m : metadata) {
      if (m.getXlinkHref() != null) gotSomeMetadata = true;
    }

    if (gotSomeMetadata) {
      out.format("
Metadata:
%n
%n");
      for (ThreddsMetadata.MetadataOther m : metadata) {
        String type = (m.getType() == null) ? "" : m.getType();
        if (m.getXlinkHref() != null) {
          String title = (m.getTitle() == null) ? "Type " + type : m.getTitle();
          String mdLink = resolveRelativeUrls ? makeHrefResolve(ds, m.getXlinkHref(), title) : makeHref(m.getXlinkHref(), null, title);
          out.format("
 %s
%n", mdLink);
        } //else {
        //out.format("

"+m.getMetadataType()+" "+m.getContentObject()+"
%n");
        //}
      }
      out.format("
%n");
    }

    java.util.List<Property> propsOrg = ds.getProperties();
    java.util.List<Property> props = new ArrayList<>(ds.getProperties().size());
    for (Property p : propsOrg) {
      if (!p.getName().startsWith("viewer"))  // eliminate the viewer properties from the html view
        props.add(p);
    }
    if (props.size() > 0) {
      out.format("
Properties:
%n
%n");
      for (Property p : props) {
        if (p.getName().equals("attachments")) { // LOOK whats this ?
          String attachLink = resolveRelativeUrls ? makeHrefResolve(ds, p.getValue(), p.getName()) : makeHref(p.getValue(), null, p.getName());
          out.format("
%s
%n", attachLink);
        } else {
          out.format("
%s = \"%s\"
%n", htmlEscaper.escape(p.getName()), htmlEscaper.escape(p.getValue()));
        }
      }
      out.format("
%n");
    }

    if (complete) out.format("");
  }

  private String rangeString(ThreddsMetadata.GeospatialRange r) {
    if (r == null) return "";
    String units = (r.getUnits() == null) ? "" : " " + r.getUnits();
    String resolution = r.hasResolution() ? " Resolution=" + r.getResolution() : "";
    return htmlEscaper.escape(r.getStart() + " to " + (r.getStart() + r.getSize()) + resolution + units);
  }

  /**
   * resolve reletive URLS against the catalog URL.
   *
   * @param ds   use ds parent catalog, if it exists
   * @param href URL to resolve
   * @return resolved URL
   */
  public String resolve(Dataset ds, String href) {
    Catalog cat = ds.getParentCatalog();
    if (cat != null) {
      try {
        java.net.URI uri = cat.resolveUri(href);
        href = uri.toString();
      } catch (java.net.URISyntaxException e) {
        return "DatasetHtmlWriter: error parsing URL= " + href;
      }
    }
    return href;
  }

  private String makeHref(String urlPath, String query, String title) {
    if (title == null) title = urlPath;
    if (query != null)
      urlPath += "?" + query;
    return "" + htmlEscaper.escape(title) + "";
  }

  private String makeHrefResolve(Dataset ds, String href, String title) {
    href = resolve(ds, href);
    return makeHref(href, null, title);
  }
}


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

package ucar.nc2.grib.grib1.tables;

import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * The purpose of this class is to read in the files from ECMWFs ecCodes software and
 * create useful grib 1 tables for the CDM. Note that the intent is to process these
 * local concept files as minimally as possible. Only run this from the git directory.
 * This is run offline
 */
public class EcmwfLocalConcepts {
    // super hash map keys
    private static final String SHORTNAME_ID = "shortName";
    private static final String DESCRIPTION_ID = "description";
    private static final String UNIT_ID = "units";

    // IDs used in localConcept files
    private static final String TABLE_VERSION_ID = "table2Version";
    private static final String PARAM_NUM_ID = "indicatorOfParameter";

    private static final Charset ENCODING = StandardCharsets.UTF_8;

    // tableNumber -> paramNumber -> metadata from table
    private final HashMap<String, HashMap<String, HashMap<String,String>>> localConcepts = new HashMap<>();

    // location of the localConcept files
    private final String ecmwfLocalConceptsLoc;

    // default constructor
    public EcmwfLocalConcepts() {
        // find path to localConcept files

        String sep = File.separator;
        String classPath = EcmwfLocalConcepts.class.getProtectionDomain().getCodeSource().getLocation().getPath();
        String split = "thredds"+sep+"grib";
        String sourcesPath = classPath.split(split)[0];
        if (classPath.equals(sourcesPath)) {
            split = "grib"+sep+"build";
            sourcesPath = classPath.split(split)[0];
            sourcesPath = classPath.split("/grib")[0];
        }
        ecmwfLocalConceptsLoc = sourcesPath+sep+"grib"+sep+"src"+sep+"main"+sep+"sources"+sep+"ecmwfEcCodes"+sep;
        // initialize input streams for reading the localConcept files
        try {
          parseLocalConcept(ecmwfLocalConceptsLoc + "shortName.def", SHORTNAME_ID);
          parseLocalConcept(ecmwfLocalConceptsLoc + "name.def", DESCRIPTION_ID);
          parseLocalConcept(ecmwfLocalConceptsLoc + "units.def", UNIT_ID);
        } catch  (IOException e) {
            e.printStackTrace();
        }
    }

    /** Parse the localConcept files needed to create grib1 tables for use by the CDM. */
    private void parseLocalConcept(String filename, String conceptName) throws IOException {
      try (InputStream is = new FileInputStream(filename)) {
        addLocalConcept(is, conceptName);
      }
    }

    /**
     * Add the information from a localConcept file to super HashMap localConcepts
     *
     * @param is InputStream of the localConcept file
     * @param conceptName "type" of localConcept being added
     */
    private void addLocalConcept(InputStream is, String conceptName) throws IOException {
        /*
        example entry from name.def:

        #Total precipitation of at least 5 mm
        'Total precipitation of at least 5 mm' = {
             table2Version = 131 ;
             indicatorOfParameter = 61 ;
            }
         */

        try (BufferedReader br = new BufferedReader(new InputStreamReader(is, ENCODING))) {
            String line = br.readLine();

            while (!line.startsWith("#"))
                line = br.readLine(); // skip

            while (true) {
                HashMap<String, String> items = new HashMap<>();
                line = br.readLine();
                if (line == null) break; // done with the file
                if ((line.length() == 0) || line.startsWith("#")) continue;
                line = cleanLine(line);
                if (line.contains("{")) {
                    String paramName = line.split("=")[0].trim();
                    line = br.readLine();
                    if (line == null) break; // done with the file
                    line = cleanLine(line);
                    while (line.contains("=")) {
                        String[] kvp = line.split("=");
                        items.put(kvp[0].trim(), kvp[1].trim());
                        line = br.readLine();
                        if (line == null) break; // done with the file
                        line = cleanLine(line);                    }
                    String tableVersion = items.get(TABLE_VERSION_ID);
                    String parameterNumber = items.get(PARAM_NUM_ID);

                    storeConcept(tableVersion, parameterNumber, conceptName, paramName);
                }
            }
        }
    }

    /**
     * clean the string representation of a line in the localConcept
     * file. Basic removal of tabs, semicolons, single quotes, etc.
     * @param lineIn  line (String) to be cleaned
     * @return cleaned version of lineIn
     */
    private String cleanLine(String lineIn) {
        String lineOut;
        lineOut = lineIn.replaceAll("'", "");
        lineOut = lineOut.replaceAll("\t", "");
        lineOut = lineOut.replaceAll(";", "");

        return lineOut.trim();
    }

    /**
     * Store localConcept information in super HashMap localConcepts
     *
     * @param tableVersion  - version of the table to add parameter to
     * @param parameterNumber  - parameter number
     * @param key Type of metadata to be added (shortName, Description, Units)
     * @param value Value of metadata
     */
    private void storeConcept(String tableVersion, String parameterNumber, String key, String value) {

        HashMap<String, HashMap<String, String>> tmpTable;
        if (localConcepts.containsKey(tableVersion)) {
            tmpTable = localConcepts.get(tableVersion);
            if (tmpTable.containsKey(parameterNumber)) {
              HashMap<String, String> tmpParam = tmpTable.get(parameterNumber);
                if (!tmpParam.containsKey(key)) {
                     tmpParam.put(key, value);
                } else {
                    System.out.println("already has key value pair: " + key + ":" + value);
                }
            } else {
                HashMap<String, String> tmpParam = new HashMap<>(4);
                tmpParam.put(key, value);
                tmpTable.put(parameterNumber, tmpParam);
            }
        } else {
            tmpTable = new HashMap<>();
            HashMap<String, String> tmpParam = new HashMap<>(4);
            tmpParam.put(key, value);
            tmpTable.put(parameterNumber, tmpParam);
        }
        localConcepts.put(tableVersion, tmpTable);
    }

    /**
     * Write out grib1 tables based on localConcepts files - these are the tables
     * that the CDM will read.
     */
    private void writeGrib1Tables() throws IOException {
        SimpleDateFormat dateFormat =
                new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz");
        Calendar cal = Calendar.getInstance();
        String writeDate = dateFormat.format(cal.getTime());
        String grib1Info;
        List<String> tableNums = new ArrayList<>();
        HashMap<String, String> paramInfo;
        Path dir = Paths.get(ecmwfLocalConceptsLoc.replace("sources/", "resources/resources/grib1/"));
        for (String tableNum : localConcepts.keySet()) {
            tableNums.add(tableNum);
            String fileName = "2.98." + tableNum + ".table";
            System.out.println("Writing: " + fileName);
            Path newFile = dir.resolve(fileName);
            Files.deleteIfExists(newFile);
            Files.createFile(newFile);

            try (BufferedWriter writer = Files.newBufferedWriter(newFile, ENCODING)){
                writer.write("# Generated by " + this.getClass().getCanonicalName() + " on " + writeDate);
                writer.newLine();
                for(String paramNum : localConcepts.get(tableNum).keySet()){
                    paramInfo = localConcepts.get(tableNum).get(paramNum);

                    String shortName = paramInfo.get(SHORTNAME_ID);
                    String description = paramInfo.get(DESCRIPTION_ID);
                    String units = paramInfo.get(UNIT_ID);

                    grib1Info = paramNum + " " + shortName + " [" + description + "] (" + units + ")";

                    writer.write(grib1Info);
                    writer.newLine();
                }
            }
        }
        writeLookupTableFile(tableNums, dir, writeDate);
    }

    /**
     * Write the lookupTables.txt file, which basically registers all of the new grib1 tables
     * with the CDM
     *
     * @param tableNums List of Table Numbers
     * @param dir Directory where the tables live
     * @param writeDate Date on which the main method of this class was run, resulting in new tables
     */
    private void writeLookupTableFile(List<String> tableNums, Path dir, String writeDate) throws IOException {

        System.out.println("Writing: lookupTables.txt");
        Collections.sort(tableNums);
        Path lookupTableReg = dir.resolve("lookupTables.txt");
        Files.deleteIfExists(lookupTableReg);
        Files.createFile(lookupTableReg);
        try (BufferedWriter writer = Files.newBufferedWriter(lookupTableReg, ENCODING)){
            writer.write("# Generated by " + this.getClass().getCanonicalName() + " on " + writeDate);
            writer.newLine();
            for(String tn : tableNums){
                String tableName = "2.98." + tn + ".table";
                String reg = "98:\t-1:\t" + tn + ":\t" + tableName;
                writer.write(reg);
                writer.newLine();
            }
        }
    }

    /**
     * Quick prinout to System.out of the different parameter metadata fields
     */
    private void showLocalConcepts() {
        for (String tableNum : localConcepts.keySet()) {
            for (String paramNum : localConcepts.get(tableNum).keySet()) {
                for (String key :localConcepts.get(tableNum).get(paramNum).keySet()) {
                   System.out.println(key + ":" + localConcepts.get(tableNum).get(paramNum).get(key));
                }
            }
        }
    }

    /**
     * Generate grib1 tables for the CDM based on the localConcept files from ECMWF GRIB-API
     *
     * @param args None
     */
    public static void main(String[] args) {

        EcmwfLocalConcepts ec = new EcmwfLocalConcepts();
        try {
            ec.writeGrib1Tables();
            System.out.println("Finished!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}


/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package thredds.server.ncss.view.dsg;

import org.springframework.http.HttpHeaders;
import thredds.server.ncss.exception.NcssException;
import thredds.server.ncss.exception.VariableNotContainedInDatasetException;
import thredds.server.ncss.format.SupportedFormat;
import ucar.nc2.VariableSimpleIF;
import ucar.nc2.ft.FeatureDataset;
import ucar.nc2.ft.FeatureDatasetPoint;
import ucar.nc2.ft2.coverage.SubsetParams;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.time.CalendarDateRange;
import ucar.nc2.time.CalendarPeriod;

import javax.servlet.http.HttpServletResponse;
import java.util.*;

/**
 * Created by cwardgar on 2014/05/20.
 */
public abstract class DsgSubsetWriter {
    protected final FeatureDatasetPoint fdPoint;
    protected final SubsetParams ncssParams;
    protected final List<VariableSimpleIF> wantedVariables;
    protected final CalendarDateRange wantedRange;

    public DsgSubsetWriter(FeatureDatasetPoint fdPoint, SubsetParams ncssParams) throws NcssException {
        this.fdPoint = fdPoint;
        this.ncssParams = ncssParams;
        this.wantedVariables = getWantedVariables(fdPoint, ncssParams);
        this.wantedRange = getWantedDateRange(ncssParams);
    }

    abstract public HttpHeaders getHttpHeaders(String datasetPath, boolean isStream);
    abstract public void write() throws Exception;

    ////////////////////////////////////////////////////// Static //////////////////////////////////////////////////////

    public void respond(HttpServletResponse res, FeatureDataset ft, String requestPathInfo, SubsetParams queryParams,
                        SupportedFormat format) throws Exception {
        write();
    }

    // TODO: This needs testing.
    public static List<VariableSimpleIF> getWantedVariables(FeatureDatasetPoint fdPoint, SubsetParams ncssParams)
            throws VariableNotContainedInDatasetException {
        List<String> vars = ncssParams.getVariables();
        if (vars.size() == 1 && vars.get(0).equals("all")) {
            return fdPoint.getDataVariables();  // Return all variables.
        }

        // restrict to these variables
        Map<String, VariableSimpleIF> dataVarsMap = new HashMap<>();
        for (VariableSimpleIF dataVar : fdPoint.getDataVariables()) {
            dataVarsMap.put(dataVar.getShortName(), dataVar);
        }

        List<String> allVarNames = new ArrayList<>(dataVarsMap.keySet());
        List<VariableSimpleIF> wantedVars = new ArrayList<>();

        for (String varName : vars) {
            if (allVarNames.contains(varName)) {
                VariableSimpleIF var = dataVarsMap.get(varName);
                wantedVars.add(var);
            } else {
                throw new VariableNotContainedInDatasetException(
                        "Variable: " + varName + " is not contained in the requested dataset");
            }
        }

        return wantedVars;
    }

    // TODO: This needs testing.
    public static CalendarDateRange getWantedDateRange(SubsetParams ncssParams) throws NcssException {
        if (ncssParams.isTrue(SubsetParams.timePresent)) {  // present
            CalendarDate time = CalendarDate.present();
            CalendarPeriod timeWindow = ncssParams.getTimeWindow();
            if (timeWindow == null) timeWindow = CalendarPeriod.Hour;
            return CalendarDateRange.of(time.subtract(timeWindow), time.add(timeWindow));

        } else if (ncssParams.getTime() != null) {  // Means we want just one single time.
            CalendarDate time = ncssParams.getTime();
            CalendarPeriod timeWindow = ncssParams.getTimeWindow();
            if (timeWindow == null) timeWindow = CalendarPeriod.Hour;

            // To prevent features from being too agressively excluded, we are accepting times that are within
            // an hour of the specified time.
            // LOOK: Do we really need the +- increment?
            //CalendarDate startR = CalendarDate.parseISOformat(null, time);
            //startR = startR.subtract(CalendarPeriod.Hour);
            //CalendarDate endR = CalendarDate.parseISOformat(null, time);
            //endR = endR.add(CalendarPeriod.Hour);
            return CalendarDateRange.of(time.subtract(timeWindow), time.add(timeWindow));

        } else if (ncssParams.getTimeRange() != null) {
            return ncssParams.getTimeRange();

        } else if (ncssParams.isTrue(SubsetParams.timeAll )) {  // Client explicitly requested all times.
            return null;   // "null" means that we want ALL times, i.e. "do not subset".

        } else {          // Client didn't specify a time parameter.
            return null;  // Do not subset.
        }
    }
}