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

/**
 * Provides support for the International System of Units (SI). A good on-line
 * reference for the SI can be found at <http://physics.nist.gov/cuu/Units/>.
 *
 * @author Steven R. Emmerson
 */
public final class SI extends UnitSystemImpl {
  private static final long serialVersionUID = 1L;

  /*
   * The singleton instance.
   *
   * @serial
   */
  private static/* final */ SI si;

	/*
   * Units corresponding to base quantities:
	 */

  /**
   * The unit corresponding to the quantity of amount of substance.
   */
  public static final Unit AMOUNT_OF_SUBSTANCE_UNIT;

  /**
   * The unit corresponding to the quantity of electric current.
   */
  public static final Unit ELECTRIC_CURRENT_UNIT;

  /**
   * The unit corresponding to the quantity of length.
   */
  public static final Unit LENGTH_UNIT;

  /**
   * The unit corresponding to the quantity of luminous intensity.
   */
  public static final Unit LUMINOUS_INTENSITY_UNIT;

  /**
   * The unit corresponding to the quantity of mass.
   */
  public static final Unit MASS_UNIT;

  /**
   * The unit corresponding to the quantity of plane angle.
   */
  public static final Unit PLANE_ANGLE_UNIT;

  /**
   * The unit corresponding to the quantity of solid angle.
   */
  public static final Unit SOLID_ANGLE_UNIT;

  /**
   * The unit corresponding to the quantity of thermodynamic temperature.
   */
  public static final Unit THERMODYNAMIC_TEMPERATURE_UNIT;

  /**
   * The unit corresponding to the quantity of time.
   */
  public static final Unit TIME_UNIT;

	/*
   * Base units:
	 */

  /**
   * Base unit of electric current. The ampere is that constant current which,
   * if maintained in two straight parallel conductors of infinite length, of
   * negligible circular cross section, and placed 1 meter apart in vacuum,
   * would produce between these conductors a force equal to 2 x 10^-7 newton
   * per meter of length.
   */
  public static final BaseUnit AMPERE;

  /**
   * Base unit of luminous intensity. The candela is the luminous intensity,
   * in a given direction, of a source that emits monochromatic radiation of
   * frequency 540 x 10^12 hertz and that has a radiant intensity in that
   * direction of (1/683) watt per steradian.
   */
  public static final BaseUnit CANDELA;

  /**
   * Base unit of thermodynamic temperature. The kelvin, unit of thermodynamic
   * temperature, is the fraction 1/273.16 of the thermodynamic temperature of
   * the triple point of water.
   */
  public static final BaseUnit KELVIN;

  /**
   * Base unit of mass. The kilogram is the unit of mass; it is equal to the
   * mass of the international prototype of the kilogram.
   */
  public static final BaseUnit KILOGRAM;

  /**
   * Base unit of length. The meter is the length of the path travelled by
   * light in vacuum during a time interval of 1/299 792 458 of a second.
   */
  public static final BaseUnit METER;              // NIST

  /**
   * Synonym for METER.
   */
  public static final BaseUnit METRE;              // ISO

  /**
   * Base unit of amount of substance. The mole is the amount of substance of
   * a system which contains as many elementary entities as there are atoms in
   * 0.012 kilogram of carbon 12.
   */
  public static final BaseUnit MOLE;

  /**
   * Base unit of time. The second is the duration of 9 192 631 770 periods of
   * the radiation corresponding to the trasition between the two hyperfine
   * levels of the ground state of the cesium-133 atom.
   */
  public static final BaseUnit SECOND;

	/*
   * Supplementary units: These units are more accurately referred to as
	 * "dimensionless derived units" -- for the purposes of this Java package,
	 * however, they are BaseUnit-s. Note, however, that their
	 * isDimensionless() method will return true.
	 */

  /**
   * Base unit of angular measure. The radian is the plane angle between two
   * radii of a circle that cut off on the circumference an arc equal in
   * length to the radius.
   */
  public static final BaseUnit RADIAN;

  /**
   * Base unit of solid angle. The steradian is the solid angle that, having
   * its vertex in the center of a sphere, cuts off an area on the surface of
   * the sphere equal to that of a square with sides of length equal to the
   * radius of the sphere.
   */
  public static final BaseUnit STERADIAN;

	/*
   * Derived units with special names and symbols:
	 */

  /**
   * Special derived unit for the quantity of frequency.
   */
  public static final Unit HERTZ;

  /**
   * Special derived unit for the quantity of force.
   */
  public static final Unit NEWTON;

  /**
   * Special derived unit for the quantity of pressure or stress.
   */
  public static final Unit PASCAL;

  /**
   * Special derived unit for the quantity of energy, work, or quantity of
   * heat.
   */
  public static final Unit JOULE;

  /**
   * Special derived unit for the quantity of power or radiant flux.
   */
  public static final Unit WATT;

  /**
   * Special derived unit for the quantity of electric charge or quantity of
   * electricity.
   */
  public static final Unit COULOMB;

  /**
   * Special derived unit for the quantity of electric potential, potential
   * difference, or electromotive force.
   */
  public static final Unit VOLT;

  /**
   * Special derived unit for the quantity of electric capacitance.
   */
  public static final Unit FARAD;

  /**
   * Special derived unit for the quantity of electric resistance.
   */
  public static final Unit OHM;

  /**
   * Special derived unit for the quantity of electric conductance.
   */
  public static final Unit SIEMENS;

  /**
   * Special derived unit for the quantity of magnetic flux.
   */
  public static final Unit WEBER;

  /**
   * Special derived unit for the quantity of magnetic flux density.
   */
  public static final Unit TESLA;

  /**
   * Special derived unit for the quantity of inductance.
   */
  public static final Unit HENRY;

  /**
   * Special derived unit for the quantity of Celsius temperature.
   */
  public static final Unit DEGREE_CELSIUS;

  /**
   * Special derived unit for the quantity of luminous flux.
   */
  public static final Unit LUMEN;

  /**
   * Special derived unit for the quantity of illuminance.
   */
  public static final Unit LUX;

	/*
	 * Derived units with special names and symbols admitted for reasons of
	 * safegarding human health.
	 */

  /**
   * Special derived unit for the quantity of activity of a radionuclide.
   */
  public static final Unit BECQUEREL;

  /**
   * Special derived unit for the quantity of absorbed dose, specific energy
   * (imparted), or kerms.
   */
  public static final Unit GRAY;

  /**
   * Special derived unit for the quantity of dose equivalent, ambient dose
   * equivalent, directional dose equivalent, personal dose equivalent, or
   * equivalent dose.
   */
  public static final Unit SIEVERT;

	/*
	 * Units outside the SI but accepted for use with the SI according to the
	 * USA National Institute of Standards and Technology (see
	 * <http://physics.nist.gov/cuu/Units/>):
	 */

  /**
   * Special derived unit -- accepted for use with the SI -- for the quantity
   * of time.
   */
  public static final Unit MINUTE;

  /**
   * Special derived unit -- accepted for use with the SI -- for the quantity
   * of time.
   */
  public static final Unit HOUR;

  /**
   * Special derived unit -- accepted for use with the SI -- for the quantity
   * of time.
   */
  public static final Unit DAY;

  /**
   * Special derived unit -- accepted for use with the SI -- for the quantity
   * of plane angle.
   */
  public static final Unit ARC_DEGREE;

  /**
   * Special derived unit -- accepted for use with the SI -- for the quantity
   * of plane angle.
   */
  public static final Unit ARC_MINUTE;

  /**
   * Special derived unit -- accepted for use with the SI -- for the quantity
   * of plane angle.
   */
  public static final Unit ARC_SECOND;

  /**
   * Special derived unit -- accepted for use with the SI -- for the quantity
   * of volume.
   */
  public static final Unit LITER;              // NIST

  /**
   * Synomym for LITER.
   */
  public static final Unit LITRE;              // ISO

  /**
   * Special derived unit -- accepted for use with the SI -- for the quantity
   * of mass.
   */
  public static final Unit METRIC_TON;          // NIST

  /**
   * Synomym for METRIC_TON.
   */
  public static final Unit TONNE;              // ISO

	/*
	 * Units outside the SI but temporarily accepted for use with the SI
	 * according to the USA National Institute of Standards and Technology (see
	 * <http://physics.nist.gov/cuu/Units/>):
	 */

  /**
   * Special derived unit -- temporarily accepted for use with the SI -- for
   * the quantity of length.
   */
  public static final Unit NAUTICAL_MILE;

  /**
   * Special derived unit -- temporarily accepted for use with the SI -- for
   * the quantity of speed.
   */
  public static final Unit KNOT;

  /**
   * Special derived unit -- temporarily accepted for use with the SI -- for
   * the quantity of length.
   */
  public static final Unit ANGSTROM;

  /**
   * Special derived unit -- temporarily accepted for use with the SI -- for
   * the quantity of area.
   */
  public static final Unit ARE;

  /**
   * Special derived unit -- temporarily accepted for use with the SI -- for
   * the quantity of area.
   */
  public static final Unit HECTARE;

  /**
   * Special derived unit -- temporarily accepted for use with the SI -- for
   * the quantity of area.
   */
  public static final Unit BARN;

  /**
   * Special derived unit -- temporarily accepted for use with the SI -- for
   * the quantity of pressure.
   */
  public static final Unit BAR;

  /**
   * Special derived unit -- temporarily accepted for use with the SI -- for
   * the quantity of acceleration.
   */
  public static final Unit GAL;

	/*
	 * Units outside the SI but temporarily accepted for use with the SI until
	 * the year 2000 according to the USA National Institute of Standards and
	 * Technology (see <http://physics.nist.gov/cuu/Units/>):
	 */

  /**
   * Special derived unit -- temporarily accepted for use with the SI until
   * the year 2000 according to the USA National Institute of Standards and
   * Technology -- for the quantity of activity of a radionuclide.
   */
  public static final Unit CURIE;

  /**
   * Special derived unit -- temporarily accepted for use with the SI until
   * the year 2000 according to the USA National Institute of Standards and
   * Technology -- for the quantity of electric charge per mass.
   */
  public static final Unit ROENTGEN;

  /**
   * Special derived unit -- temporarilty accepted for use with the SI until
   * the year 2000 according to the USA National Institute of Standards and
   * Technology -- for the quantity of absorbed dose.
   */
  public static final Unit RAD;

  /**
   * Special derived unit -- temporarilty accepted for use with the SI until
   * the year 2000 according to the USA National Institute of Standards and
   * Technology -- for the quantity of equivalent absorbed dose.
   */
  public static final Unit REM;

  static {
    BaseUnit ampere = null;
    BaseUnit candela = null;
    BaseUnit kelvin = null;
    BaseUnit kilogram = null;
    BaseUnit meter = null;
    BaseUnit mole = null;
    BaseUnit second = null;
    BaseUnit radian = null;
    BaseUnit steradian = null;
    Unit hertz = null;
    Unit newton = null;
    Unit pascal = null;
    Unit joule = null;
    Unit watt = null;
    Unit coulomb = null;
    Unit volt = null;
    Unit farad = null;
    Unit ohm = null;
    Unit siemens = null;
    Unit weber = null;
    Unit tesla = null;
    Unit henry = null;
    Unit degree_celsius = null;
    Unit lumen = null;
    Unit lux = null;
    Unit becquerel = null;
    Unit gray = null;
    Unit sievert = null;
    Unit minute = null;
    Unit hour = null;
    Unit day = null;
    Unit arc_degree = null;
    Unit arc_minute = null;
    Unit arc_second = null;
    Unit liter = null;
    Unit metric_ton = null;
    Unit nautical_mile = null;
    Unit knot = null;
    Unit angstrom = null;
    Unit are = null;
    Unit hectare = null;
    Unit barn = null;
    Unit bar = null;
    Unit gal = null;
    Unit curie = null;
    Unit roentgen = null;
    Unit rad = null;
    Unit rem = null;

    try {
      ampere = bu("ampere", "A", BaseQuantity.ELECTRIC_CURRENT);
      candela = bu("candela", "cd", BaseQuantity.LUMINOUS_INTENSITY);
      kelvin = bu("kelvin", "K", BaseQuantity.THERMODYNAMIC_TEMPERATURE);
      kilogram = bu("kilogram", "kg", BaseQuantity.MASS);
      meter = bu("meter", "m", BaseQuantity.LENGTH);
      mole = bu("mole", "mol", BaseQuantity.AMOUNT_OF_SUBSTANCE);
      second = bu("second", "s", BaseQuantity.TIME);
      radian = bu("radian", "rad", BaseQuantity.PLANE_ANGLE);
      steradian = bu("steradian", "sr", BaseQuantity.SOLID_ANGLE);

      hertz = du("hertz", "Hz", second.raiseTo(-1));
      newton = du("newton", "N", kilogram.multiplyBy(meter).divideBy(
              second.raiseTo(2)));
      pascal = du("pascal", "Pa", newton.divideBy(meter.raiseTo(2)));
      joule = du("joule", "J", newton.multiplyBy(meter));
      watt = du("watt", "W", joule.divideBy(second));
      coulomb = du("coulomb", "C", ampere.multiplyBy(second));
      volt = du("volt", "V", watt.divideBy(ampere));
      farad = du("farad", "F", coulomb.divideBy(volt));
      ohm = du("ohm", "Ohm", volt.divideBy(ampere));
      siemens = du("siemens", "S", ohm.raiseTo(-1));
      weber = du("weber", "Wb", volt.multiplyBy(second));
      tesla = du("tesla", "T", weber.divideBy(meter.raiseTo(2)));
      henry = du("henry", "H", weber.divideBy(ampere));
      degree_celsius = du("degree celsius", "Cel", new OffsetUnit(kelvin,
              273.15));
      lumen = du("lumen", "lm", candela.multiplyBy(steradian));
      lux = du("lux", "lx", lumen.divideBy(meter.raiseTo(2)));

      becquerel = du("becquerel", "Bq", hertz);
      gray = du("gray", "Gy", joule.divideBy(kilogram));
      sievert = du("sievert", "Sv", joule.divideBy(kilogram));

      minute = du("minute", "min", new ScaledUnit(60, second));
      hour = du("hour", "h", new ScaledUnit(60, minute));
      day = du("day", "d", new ScaledUnit(24, hour));
      arc_degree = du("arc degree", "deg", new ScaledUnit(Math.PI / 180,
              radian));
      arc_minute = du("arc minute", "'", new ScaledUnit(1. / 60.,
              arc_degree));
      arc_second = du("arc second", "\"", new ScaledUnit(1. / 60.,
              arc_minute));
      liter = du("liter", "L", new ScaledUnit(1e-3, meter.raiseTo(3)));
      // exact. However, from 1901 to 1964, 1 liter = 1.000028 dm3
      metric_ton = du("metric ton", "t", new ScaledUnit(1e3, kilogram));

      nautical_mile = du("nautical mile", "nmi", new ScaledUnit(1852,
              meter));
      knot = du("knot", "kt", nautical_mile.divideBy(hour));
      angstrom = du("angstrom", null, new ScaledUnit(1e-10, meter));
      are = du("are", "are", new ScaledUnit(10, meter).raiseTo(2));
      hectare = du("hectare", "ha", new ScaledUnit(100, are));
      barn = du("barn", "b", new ScaledUnit(1e-28, meter.raiseTo(2)));
      bar = du("bar", "bar", new ScaledUnit(1e5, pascal));
      gal = du("gal", "Gal", new ScaledUnit(1e-2, meter).divideBy(second
              .raiseTo(2)));

      curie = du("curie", "Ci", new ScaledUnit(3.7e10, becquerel));
      roentgen = du("roentgen", "R", new ScaledUnit(2.58e-4, coulomb
              .divideBy(kilogram)));
      rad = du("rad", "rd", new ScaledUnit(1e-2, gray));
      rem = du("rem", "rem", new ScaledUnit(1e-2, sievert));
    } catch (final UnitException e) {
      final String reason = e.getMessage();
      System.err.println("Couldn't initialize class SI" + reason == null
              ? ""
              : (": " + reason));
    }

    AMOUNT_OF_SUBSTANCE_UNIT = mole;
    ELECTRIC_CURRENT_UNIT = ampere;
    LENGTH_UNIT = meter;
    LUMINOUS_INTENSITY_UNIT = candela;
    MASS_UNIT = kilogram;
    PLANE_ANGLE_UNIT = radian;
    SOLID_ANGLE_UNIT = steradian;
    THERMODYNAMIC_TEMPERATURE_UNIT = kelvin;
    TIME_UNIT = second;

    AMPERE = ampere;
    CANDELA = candela;
    KELVIN = kelvin;
    KILOGRAM = kilogram;
    METER = meter;
    METRE = METER;
    MOLE = mole;
    SECOND = second;
    RADIAN = radian;
    STERADIAN = steradian;
    HERTZ = hertz;
    NEWTON = newton;
    PASCAL = pascal;
    JOULE = joule;
    WATT = watt;
    COULOMB = coulomb;
    VOLT = volt;
    FARAD = farad;
    OHM = ohm;
    SIEMENS = siemens;
    WEBER = weber;
    TESLA = tesla;
    HENRY = henry;
    DEGREE_CELSIUS = degree_celsius;
    LUMEN = lumen;
    LUX = lux;
    BECQUEREL = becquerel;
    GRAY = gray;
    SIEVERT = sievert;
    MINUTE = minute;
    HOUR = hour;
    DAY = day;
    ARC_DEGREE = arc_degree;
    ARC_MINUTE = arc_minute;
    ARC_SECOND = arc_second;
    LITER = liter;
    LITRE = LITER;
    METRIC_TON = metric_ton;
    TONNE = METRIC_TON;
    NAUTICAL_MILE = nautical_mile;
    KNOT = knot;
    ANGSTROM = angstrom;
    ARE = are;
    HECTARE = hectare;
    BARN = barn;
    BAR = bar;
    GAL = gal;
    CURIE = curie;
    ROENTGEN = roentgen;
    RAD = rad;
    REM = rem;
  }

  /**
   * Factory method for constructing a base unit.
   *
   * @param name     The name of the unit.
   * @param symbol   The symbol for the unit.
   * @param quantity The base quantity of the unit.
   * @return The base unit corresponding to the arguments.
   */
  private static BaseUnit bu(final String name, final String symbol,
                             final BaseQuantity quantity) throws NameException,
          UnitExistsException {
    return BaseUnit.getOrCreate(UnitName.newUnitName(name, null, symbol),
            quantity);
  }

  /**
   * Factory method for constructing a derived unit.
   *
   * @param name       The name of the unit.
   * @param symbol     The symbol for the unit.
   * @param definition The definition of the unit.
   * @return The derived unit corresponding to the arguments.
   */
  private static Unit du(final String name, final String symbol,
                         final Unit definition) throws NameException {
    return definition.clone(UnitName.newUnitName(name, null, symbol));
  }

  /**
   * Constructs an SI system of units from nothing.
   */
  private SI() throws UnitExistsException, NameException, PrefixDBException,
          NoSuchUnitException {
    super(baseUnitDB(), derivedUnitDB());
  }

  /**
   * Returns the base unit database of the SI.
   *
   * @return The base unit database of the SI.
   * @throws NameException       Bad unit name.
   * @throws UnitExistsException Attempt to redefine an existing unit.
   * @throws NoSuchUnitException Necessary unit not found.
   */
  private static UnitDBImpl baseUnitDB() throws NameException, UnitExistsException, NoSuchUnitException {
    final UnitDBImpl db = new UnitDBImpl(9, 9);
    db.addUnit(AMPERE);
    db.addUnit(CANDELA);
    db.addUnit(KELVIN);
    db.addUnit(KILOGRAM);
    db.addUnit(METER);
    db.addUnit(MOLE);
    db.addUnit(SECOND);
    db.addUnit(RADIAN);
    db.addUnit(STERADIAN);
    db.addAlias("metre", "meter");
    return db;
  }

  /**
   * Returns the derived unit database of the SI.
   *
   * @return The derived unit database of the SI.
   * @throws NameException       Bad unit name.
   * @throws UnitExistsException Attempt to redefine an existing unit.
   * @throws NoSuchUnitException Necessary unit not found.
   */
  private static UnitDBImpl derivedUnitDB() throws NameException, UnitExistsException, NoSuchUnitException {
    final UnitDBImpl db = new UnitDBImpl(42, 43);

    db.addUnit(HERTZ);
    db.addUnit(NEWTON);
    db.addUnit(PASCAL);
    db.addUnit(JOULE);
    db.addUnit(WATT);
    db.addUnit(COULOMB);
    db.addUnit(VOLT);
    db.addUnit(FARAD);
    db.addUnit(OHM);
    db.addUnit(SIEMENS);
    db.addUnit(WEBER);
    db.addUnit(TESLA);
    db.addUnit(HENRY);
    db.addUnit(DEGREE_CELSIUS);
    db.addUnit(LUMEN);
    db.addUnit(LUX);

    db.addUnit(BECQUEREL);
    db.addUnit(GRAY);
    db.addUnit(SIEVERT);

    db.addUnit(MINUTE);
    db.addUnit(HOUR);
    db.addUnit(DAY);
    db.addUnit(ARC_DEGREE);
    db.addUnit(ARC_MINUTE);
    db.addUnit(ARC_SECOND);
    db.addUnit(LITER);
    db.addUnit(METRIC_TON);

    db.addUnit(NAUTICAL_MILE);
    db.addUnit(KNOT);
    db.addUnit(ANGSTROM);
    db.addUnit(ARE);
    db.addUnit(HECTARE);
    db.addUnit(BARN);
    db.addUnit(BAR);
    db.addUnit(GAL);

    db.addUnit(CURIE);
    db.addUnit(ROENTGEN);
    db.addUnit(RAD);
    db.addUnit(REM);

    db.addAlias("litre", "liter", "l");
    db.addAlias("tonne", "metric ton");
    db.addSymbol("tne", "tonne");

    return db;
  }

  /**
   * Returns an instance of the SI system of units.
   *
   * @return An instance of the SI system of units.
   * @throws UnitSystemException Couldn't create an instance of the SI system of units.
   */
  public static synchronized SI instance() throws UnitSystemException {
    if (si == null) {
      try {
        si = new SI();
      } catch (final UnitException e) {
        throw new UnitSystemException("Couldn't initialize class SI", e);
      }
    }
    return si;
  }

}


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

import org.slf4j.Logger;
import thredds.client.catalog.Catalog;
import thredds.client.catalog.Dataset;
import thredds.client.catalog.Documentation;
import thredds.client.catalog.ThreddsMetadata;
import thredds.client.catalog.builder.CatalogBuilder;
import thredds.client.catalog.builder.CatalogRefBuilder;
import thredds.client.catalog.builder.DatasetBuilder;
import thredds.inventory.CollectionUpdateType;
import thredds.inventory.MFileCollectionManager;
import thredds.server.catalog.FeatureCollectionRef;
import thredds.server.catalog.writer.ThreddsMetadataExtractor;
import ucar.nc2.constants.FeatureType;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.dt.GridDataset;
import ucar.nc2.ft.fmrc.Fmrc;
import ucar.nc2.ft2.coverage.CoverageCollection;
import ucar.nc2.ft2.coverage.FeatureDatasetCoverage;
import ucar.nc2.ft2.coverage.adapter.DtCoverageAdapter;
import ucar.nc2.ft2.coverage.adapter.DtCoverageDataset;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.time.CalendarDateRange;
import ucar.nc2.units.DateRange;
import ucar.unidata.util.StringUtil2;

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

/**
 * InvDataset Feature Collection for Fmrc
 * Generate anew each call; use object caching if needed to improve efficiency
 *
 * @author caron
 * @since Mar 3, 2010
 */

@ThreadSafe
public class InvDatasetFcFmrc extends InvDatasetFeatureCollection {
  static private final Logger logger = org.slf4j.LoggerFactory.getLogger(InvDatasetFcFmrc.class);

  static private final String FMRC = "fmrc.ncd";
  static private final String BEST = "best.ncd";

  static private final String RUNS = "runs";
  static private final String RUN_NAME = "RUN_";
  static private final String RUN_TITLE = "Forecast Model Run";

  static private final String FORECAST = "forecast";
  static private final String FORECAST_NAME = "ConstantForecast_";
  static private final String FORECAST_TITLE = "Constant Forecast Date";

  static private final String OFFSET = "offset";
  static private final String OFFSET_NAME = "Offset_";
  static private final String OFFSET_TITLE = "Constant Forecast Offset";

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

  private final Fmrc fmrc;
  private final Set<FeatureCollectionConfig.FmrcDatasetType> wantDatasets;

  InvDatasetFcFmrc(FeatureCollectionRef parent, FeatureCollectionConfig config) {
    super(parent, config);
    makeCollection();
    // check if FMRC is empty, and if so, throw excpetion and log it.
    boolean filesFound = datasetCollection.getFilesSorted().iterator().hasNext();
    if (!filesFound) {
      String collectionName = parent.getCollectionName();
      String spec = config.spec;
      String catalog = parent.getParentCatalog().getUriString();
      String logMsg = String.format("The FMRC %s defined in %s cannot find any files matching the spec %s.",
                                    collectionName, catalog,spec);
      logger.warn(logMsg);
      String errMsg = String.format("The FMRC %s cannot find any files in the collection.", collectionName);
      throw new RuntimeException(errMsg);
    }

    Formatter errlog = new Formatter();
    try {
      fmrc = new Fmrc(datasetCollection, config);
    } catch (Exception e) {
      throw new RuntimeException(errlog.toString());
    }

    this.wantDatasets = config.fmrcConfig.datasets;

    state = new State(null);
  }

  @Override
  public void close() {
    if (fmrc != null)
      fmrc.close();
    super.close();
  }

  /* @Override  // overriding superclass -WHY?
  public void update(CollectionUpdateType force) {
    fmrc.update();       // so when is work done?
  } */

  protected void update(CollectionUpdateType force) throws IOException {  // this may be called from a background thread, or from checkState() request thread
    logger.debug("update {} force={}", name, force);
    boolean changed;
    MFileCollectionManager dcm;
    switch (force) {
      case always:
      case test:
        dcm = (MFileCollectionManager) getDatasetCollectionManager();
        changed = dcm.scan(false);
        if (changed)
          super.update(force);
        break;

      case never:
        return;

      default:
        super.update(force);
    }
  }

  public void updateProto() {
    fmrc.updateProto();
  }

  @Override
  protected void updateCollection(State localState, CollectionUpdateType force) {  // LOOK probably not right
    try {
      fmrc.update();

      boolean checkInv = fmrc.checkInvState(localState.lastInvChange);
      boolean checkProto = fmrc.checkProtoState(localState.lastProtoChange);

      if (checkProto) {
        // add Variables, GeospatialCoverage, TimeCoverage
        GridDataset gds = fmrc.getDataset2D(null);
        if (null != gds) {
          ThreddsMetadataExtractor extractor = new ThreddsMetadataExtractor();
          localState.vars = extractor.extractVariables(null, gds);
          localState.coverage = extractor.extractGeospatial(gds);
          localState.dateRange = extractor.extractCalendarDateRange(gds);
        }
        localState.lastProtoChange = System.currentTimeMillis();
      }

      if (checkInv) {
        // makeDatasetTop(localState);
        localState.lastInvChange = System.currentTimeMillis();
      }

    } catch (IOException e) {
      e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
    }

  }

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

  // called by DataRootHandler.makeDynamicCatalog() when the catref is requested

  @Override
  public CatalogBuilder makeCatalog(String match, String orgPath, URI catURI) throws IOException {
    logger.debug("FMRC make catalog for " + match + " " + catURI);
    State localState = checkState();

    try {
      if ((match == null) || (match.length() == 0)) {
        return makeCatalogTop(catURI, localState);

      } else if (match.equals(RUNS) && wantDatasets.contains(FeatureCollectionConfig.FmrcDatasetType.Runs))
        return makeCatalogRuns(catURI, localState);

      else if (match.equals(OFFSET) && wantDatasets.contains(FeatureCollectionConfig.FmrcDatasetType.ConstantOffsets))
        return makeCatalogOffsets(catURI, localState);

      else if (match.equals(FORECAST) && wantDatasets.contains(FeatureCollectionConfig.FmrcDatasetType.ConstantForecasts))
        return makeCatalogForecasts(catURI, localState);

      else if (match.startsWith(FILES) && wantDatasets.contains(FeatureCollectionConfig.FmrcDatasetType.Files)) {
        return makeCatalogFiles(catURI, localState, datasetCollection.getFilenames(), true);
      }

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

    return null;
  }

  private CatalogBuilder makeCatalog(URI catURI, State localState, String name) throws IOException {
    Catalog parentCatalog = parent.getParentCatalog();

    CatalogBuilder result = new CatalogBuilder();
    result.setName(makeFullName(parent));
    result.setVersion(parentCatalog.getVersion());
    result.setBaseURI(catURI);                       // LOOK is catURI right ??
    result.addService(virtualService);

    DatasetBuilder top = new DatasetBuilder(null);
    top.transferInheritedMetadata(parent); // make all inherited metadata local
    top.setName(name);

    ThreddsMetadata tmi = top.getInheritableMetadata();
    tmi.set(Dataset.ServiceName, virtualService.getName());
    if (localState.coverage != null) tmi.set(Dataset.GeospatialCoverage, localState.coverage);
    if (localState.dateRange != null) tmi.set(Dataset.TimeCoverage, localState.dateRange);
    if (localState.vars != null) tmi.set(Dataset.VariableGroups, localState.vars);

    result.addDataset(top);

    for (DatasetBuilder ds : makeRunDatasets(top))
      top.addDataset(ds);

    return result;
  }

  private CatalogBuilder makeCatalogRuns(URI catURI, State localState) throws IOException {
    CatalogBuilder runCatalog = makeCatalog(catURI, localState, RUN_TITLE);
    DatasetBuilder top = runCatalog.getTopDataset();

    if (top != null)
      for (DatasetBuilder ds : makeRunDatasets(top))
        top.addDataset(ds);

    return runCatalog;
  }

  private CatalogBuilder makeCatalogOffsets(URI catURI, State localState) throws IOException {
    CatalogBuilder offCatalog = makeCatalog(catURI, localState, OFFSET_TITLE);
    DatasetBuilder top = offCatalog.getTopDataset();

    if (top != null)
      for (DatasetBuilder ds : makeOffsetDatasets(top))
        top.addDataset(ds);

    return offCatalog;
  }

  private CatalogBuilder makeCatalogForecasts(URI catURI, State localState) throws IOException {
    CatalogBuilder offCatalog = makeCatalog(catURI, localState, OFFSET_TITLE);
    DatasetBuilder top = offCatalog.getTopDataset();

    if (top != null)
      for (DatasetBuilder ds : makeOffsetDatasets(top))
        top.addDataset(ds);

    return offCatalog;
  }

  private List<DatasetBuilder> makeRunDatasets(DatasetBuilder parent) throws IOException {
    List<DatasetBuilder> datasets = new ArrayList<>();

    for (CalendarDate runDate : fmrc.getRunDates()) {
      String myname = name + "_" + RUN_NAME + runDate;
      myname = StringUtil2.replace(myname, ' ', "_");

      DatasetBuilder nested = new DatasetBuilder(parent);
      nested.setName(myname);
      nested.put(Dataset.UrlPath, this.configPath + "/" + RUNS + "/" + myname);
      nested.put(Dataset.Id, this.configPath + "/" + RUNS + "/" + myname);
      nested.addToList(Dataset.Documentation, new Documentation(null, null, null, "summary", "Data from Run " + myname));
      CalendarDateRange cdr = fmrc.getDateRangeForRun(runDate);
      if (cdr != null)
        nested.put(Dataset.TimeCoverage, new DateRange(cdr));
      datasets.add(nested);
    }

    Collections.reverse(datasets);
    return datasets;
  }

  private List<DatasetBuilder> makeOffsetDatasets(DatasetBuilder parent) throws IOException {
    List<DatasetBuilder> datasets = new ArrayList<>();

    for (double offset : fmrc.getForecastOffsets()) {
      String myname = name + "_" + OFFSET_NAME + offset + "hr";
      myname = StringUtil2.replace(myname, ' ', "_");

      DatasetBuilder nested = new DatasetBuilder(parent);
      nested.setName(myname);
      nested.put(Dataset.UrlPath, this.configPath + "/" + OFFSET + "/" + myname);
      nested.put(Dataset.Id, this.configPath + "/" + OFFSET + "/" + myname);
      nested.addToList(Dataset.Documentation, new Documentation(null, null, null, "summary", "Data from the " + offset + " hour forecasts, across different model runs."));
      CalendarDateRange cdr = fmrc.getDateRangeForOffset(offset);
      if (cdr != null)
        nested.put(Dataset.TimeCoverage, new DateRange(cdr));
      datasets.add(nested);
    }

    return datasets;
  }

  private List<DatasetBuilder> makeForecastDatasets(DatasetBuilder parent) throws IOException {

    List<DatasetBuilder> datasets = new ArrayList<>();

    for (CalendarDate forecastDate : fmrc.getForecastDates()) {
      String myname = name + "_" + FORECAST_NAME + forecastDate;
      myname = StringUtil2.replace(myname, ' ', "_");

      DatasetBuilder nested = new DatasetBuilder(parent);
      nested.setName(myname);
      nested.put(Dataset.UrlPath, this.configPath + "/" + FORECAST + "/" + myname);
      nested.put(Dataset.Id, this.configPath + "/" + FORECAST + "/" + myname);
      nested.addToList(Dataset.Documentation, new Documentation(null, null, null, "summary", "Data with the same forecast date, " + name + ", across different model runs."));
      nested.put(Dataset.TimeCoverage, new DateRange(CalendarDateRange.of(forecastDate, forecastDate)));
      datasets.add(nested);
    }

    return datasets;
  }

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

  @Override
  protected DatasetBuilder makeDatasetTop(URI catURI, State localState) {
    DatasetBuilder top = new DatasetBuilder(null);
    top.transferInheritedMetadata(parent); // make all inherited metadata local
    top.setName(name);
    top.addServiceToCatalog(virtualService);

    ThreddsMetadata tmi = top.getInheritableMetadata();  // LOOK allow to change ??
    tmi.set(Dataset.FeatureType, FeatureType.GRID.toString()); // override GRIB
    tmi.set(Dataset.ServiceName, virtualService.getName());
    if (localState.coverage != null) tmi.set(Dataset.GeospatialCoverage, localState.coverage);
    if (localState.dateRange != null) tmi.set(Dataset.TimeCoverage, localState.dateRange);
    if (localState.vars != null) tmi.set(Dataset.VariableGroups, localState.vars);

    if (wantDatasets.contains(FeatureCollectionConfig.FmrcDatasetType.TwoD)) {
      DatasetBuilder twoD = new DatasetBuilder(top);
      twoD.setName("Forecast Model Run Collection (2D time coordinates)");
      String myname = name + "_" + FMRC;
      myname = StringUtil2.replace(myname, ' ', "_");
      twoD.put(Dataset.UrlPath, this.configPath + "/" + myname);
      twoD.put(Dataset.Id, this.configPath + "/" + myname);
      twoD.addToList(Dataset.Documentation, new Documentation(null, null, null, "summary", "Forecast Model Run Collection (2D time coordinates)."));
      top.addDataset(twoD);
    }

    if (wantDatasets.contains(FeatureCollectionConfig.FmrcDatasetType.Best)) {
      DatasetBuilder best = new DatasetBuilder(top);
      best.setName("Best Time Series");
      String myname = name + "_" + BEST;
      myname = StringUtil2.replace(myname, ' ', "_");
      best.put(Dataset.UrlPath, this.configPath + "/" + myname);
      best.put(Dataset.Id, this.configPath + "/" + myname);
      best.addToList(Dataset.Documentation, new Documentation(null, null, null, "summary", "Best time series, taking the data from the most recent run available."));
      top.addDataset(best);
    }

    if (config.fmrcConfig.getBestDatasets() != null) {
      for (FeatureCollectionConfig.BestDataset bd : config.fmrcConfig.getBestDatasets()) {
        DatasetBuilder ds = new DatasetBuilder(top);
        ds.setName(bd.name);
        String myname = name + "_" + bd.name;
        myname = StringUtil2.replace(myname, ' ', "_");
        ds.put(Dataset.UrlPath, this.configPath + "/" + myname);
        ds.put(Dataset.Id, this.configPath + "/" + myname);
        ds.addToList(Dataset.Documentation, new Documentation(null, null, null, "summary", "Best time series, excluding offset hours less than " + bd.greaterThan));
        top.addDataset(ds);
      }
    }

    if (wantDatasets.contains(FeatureCollectionConfig.FmrcDatasetType.Runs)) {
      CatalogRefBuilder ds = new CatalogRefBuilder(top);
      ds.setTitle(RUN_TITLE);
      ds.setHref(getCatalogHref(RUNS));
      top.addDataset(ds);
    }

    if (wantDatasets.contains(FeatureCollectionConfig.FmrcDatasetType.ConstantForecasts)) {
      CatalogRefBuilder ds = new CatalogRefBuilder(top);
      ds.setTitle(FORECAST_TITLE);
      ds.setHref(getCatalogHref(FORECAST));
      top.addDataset(ds);
    }

    if (wantDatasets.contains(FeatureCollectionConfig.FmrcDatasetType.ConstantOffsets)) {
      CatalogRefBuilder ds = new CatalogRefBuilder(top);
      ds.setTitle(OFFSET_TITLE);
      ds.setHref(getCatalogHref(OFFSET));
      top.addDataset(ds);
    }

    if (wantDatasets.contains(FeatureCollectionConfig.FmrcDatasetType.Files) && (topDirectory != null)) {
      CatalogRefBuilder ds = new CatalogRefBuilder(top);
      ds.setTitle(FILES);
      ds.setHref(getCatalogHref(FILES));
      top.addDataset(ds);
    }

    return top;
  }

  public CoverageCollection getGridCoverage(String matchPath) throws IOException {
    NetcdfDataset ncd = getNetcdfDataset(matchPath);
    if (ncd == null) return null;
    DtCoverageDataset gds = new DtCoverageDataset(ncd);
    FeatureDatasetCoverage cc = DtCoverageAdapter.factory(gds, new Formatter());
    if (cc == null) return null;

    assert cc.getCoverageCollections().size() == 1;  // LOOK probably want to use endpoint#datasetName ?
    return cc.getCoverageCollections().get(0);
  }

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

    int pos = matchPath.indexOf('/');
    String wantType = (pos > -1) ? matchPath.substring(0, pos) : matchPath;
    String wantName = (pos > -1) ? matchPath.substring(pos + 1) : matchPath;
    String hasName = StringUtil2.replace(name, ' ', "_") + "_";

    try {
      if (wantType.equalsIgnoreCase(FILES)) {
        NetcdfDataset ncd = getNetcdfDataset(matchPath);
        return ncd == null ? null : new ucar.nc2.dt.grid.GridDataset(ncd);

      } else if (wantName.equals(hasName + FMRC) && wantDatasets.contains(FeatureCollectionConfig.FmrcDatasetType.TwoD)) {
        return fmrc.getDataset2D(null);

      } else if (wantName.equals(hasName + BEST) && wantDatasets.contains(FeatureCollectionConfig.FmrcDatasetType.Best)) {
        return fmrc.getDatasetBest();

      } else if (wantType.equals(OFFSET) && wantDatasets.contains(FeatureCollectionConfig.FmrcDatasetType.ConstantOffsets)) {
        int pos1 = wantName.lastIndexOf(OFFSET_NAME);
        int pos2 = wantName.lastIndexOf("hr");
        if ((pos1 < 0) || (pos2 < 0)) return null;
        String id = wantName.substring(pos1 + OFFSET_NAME.length(), pos2);
        try {
          double hour = Double.parseDouble(id);
          return fmrc.getConstantOffsetDataset(hour);
        } catch (NumberFormatException e) {
          return null; // user input error
        }

      } else if (wantType.equals(RUNS) && wantDatasets.contains(FeatureCollectionConfig.FmrcDatasetType.Runs)) {
        int pos1 = wantName.indexOf(RUN_NAME);
        if (pos1 < 0) return null;
        String id = wantName.substring(pos1 + RUN_NAME.length());

        CalendarDate date = CalendarDate.parseISOformat(null, id);
        if (date == null) return null; // user input error
        return fmrc.getRunTimeDataset(date);

      } else if (wantType.equals(FORECAST) && wantDatasets.contains(FeatureCollectionConfig.FmrcDatasetType.ConstantForecasts)) {
        int pos1 = wantName.indexOf(FORECAST_NAME);
        if (pos1 < 0) return null;
        String id = wantName.substring(pos1 + FORECAST_NAME.length());

        CalendarDate date = CalendarDate.parseISOformat(null, id);
        if (date == null) return null; // user input error
        return fmrc.getConstantForecastDataset(date);

      } else if (config.fmrcConfig.getBestDatasets() != null) {
        for (FeatureCollectionConfig.BestDataset bd : config.fmrcConfig.getBestDatasets()) {
          if (wantName.endsWith(bd.name)) {
            return fmrc.getDatasetBest(bd);
          }
        }
      }

    } catch (FileNotFoundException e) {
      return null;
    }

    return null;
  }

}


/*
 * 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.ma2.InvalidRangeException;
import ucar.ma2.Range;
import ucar.ma2.RangeIterator;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.time.CalendarDateRange;
import ucar.nc2.util.Misc;

import javax.annotation.Nonnull;
import java.util.Arrays;

import ucar.nc2.util.Optional;

/**
 * Helper class for CoverageCoordAxis1D for subsetting and searching.
 *
 * @author caron
 * @since 7/13/2015
 */
class CoordAxisHelper {
  private final CoverageCoordAxis1D axis;

  CoordAxisHelper(CoverageCoordAxis1D axis) {
    this.axis = axis;
  }

  /**
   * Given a coordinate interval, find what grid element matches it.
   * @param target  interval in this coordinate system
   * @param bounded if true, always return a valid index. otherwise can return < 0 or > n-1
   * @return index of grid point containing it, or < 0 or > n-1 if outside grid area
   */
  int findCoordElement(double[] target, boolean bounded) {
    switch (axis.getSpacing()) {
      case regularInterval:
        // can use midpoint
        return findCoordElementRegular((target[0]+target[1])/2, bounded);
      case contiguousInterval:
        // can use midpoint
        return findCoordElementContiguous((target[0]+target[1])/2, bounded);
      case discontiguousInterval:
        // cant use midpoint
        return findCoordElementDiscontiguousInterval(target, bounded);
    }
    throw new IllegalStateException("unknown spacing" + axis.getSpacing());
  }

  /**
   * Given a coordinate position, find what grid element contains it.
   * This means that
   *

   * edge[i] <= target < edge[i+1] (if values are ascending)
   * edge[i] > target >= edge[i+1] (if values are descending)
   *

   *
   * @param target  position in this coordinate system
   * @param bounded if true, always return a valid index. otherwise can return < 0 or > n-1
   * @return index of grid point containing it, or < 0 or > n-1 if outside grid area
   */
  int findCoordElement(double target, boolean bounded) {
    switch (axis.getSpacing()) {
      case regularInterval:
      case regularPoint:
        return findCoordElementRegular(target, bounded);
      case irregularPoint:
      case contiguousInterval:
        return findCoordElementContiguous(target, bounded);
      case discontiguousInterval:
        return findCoordElementDiscontiguousInterval(target, bounded);
    }
    throw new IllegalStateException("unknown spacing" + axis.getSpacing());
  }

  // same contract as findCoordElement()
  private int findCoordElementRegular(double coordValue, boolean bounded) {
    int n = axis.getNcoords();
    if (n == 1 && bounded) return 0;

    double distance = coordValue - axis.getCoordEdge1(0);
    double exactNumSteps = distance / axis.getResolution();
    //int index = (int) Math.round(exactNumSteps); // ties round to +Inf
    int index = (int) exactNumSteps; // truncate down

    if (bounded && index < 0) return 0;
    if (bounded && index >= n) return n - 1;

    // check that found point is within interval
    if (index >= 0 && index < n) {
      double lower = axis.getCoordEdge1(index);
      double upper = axis.getCoordEdge2(index);
      if (axis.isAscending()) {
        assert lower <= coordValue : lower + " should be le " + coordValue;
        assert upper >= coordValue : upper + " should be ge " + coordValue;
      } else {
        assert lower >= coordValue : lower + " should be ge " + coordValue;
        assert upper <= coordValue : upper + " should be le " + coordValue;
      }
    }

    return index;
  }

  /**
   * Performs a binary search to find the index of the element of the array whose value is contained in the contiguous intervals.
   * irregularPoint,    // irregular spaced points (values, npts), edges halfway between coords
   * contiguousInterval, // irregular contiguous spaced intervals (values, npts), values are the edges, and there are npts+1, coord halfway between edges
   *

   * same contract as findCoordElement()
   */
  private int findCoordElementContiguous(double target, boolean bounded) {
    int n = axis.getNcoords();
    //double resolution = (values[n-1] - values[0]) / (n - 1);
    //int startGuess = (int) Math.round((target - values[0]) / resolution);

    int low = 0;
    int high = n - 1;
    if (axis.isAscending()) {
      // Check that the point is within range
      if (target < axis.getCoordEdge1(0))
        return bounded ? 0 : -1;
      else if (target > axis.getCoordEdgeLast())
        return bounded ? n - 1 : n;

      // do a binary search to find the nearest index
      int mid;
      while (high > low + 1) {
        mid = (low + high) / 2;                           // binary search
        if (contains(target, mid, true)) return mid;
        else if (axis.getCoordEdge2(mid) < target) low = mid;
        else high = mid;
      }
      return contains(target, low, true) ? low : high;

    } else {  // descending

      // Check that the point is within range
      if (target > axis.getCoordEdge1(0))
        return bounded ? 0 : -1;
      else if (target < axis.getCoordEdgeLast())
        return bounded ? n - 1 : n;

      // do a binary search to find the nearest index
      int mid;
      while (high > low + 1) {
        mid = (low + high) / 2;         // binary search
        if (contains(target, mid, false)) return mid;
        else if (axis.getCoordEdge2(mid) < target) high = mid;
        else low = mid;
      }
      return contains(target, low, false) ? low : high;
    }
  }

  private boolean contains(double target, int coordIdx, boolean ascending) {
    double midVal1 = axis.getCoordEdge1(coordIdx);
    double midVal2 = axis.getCoordEdge2(coordIdx);
    if (ascending)
      return (midVal1 <= target && target <= midVal2);
    else
      return (midVal1 >= target && target >= midVal2);
  }

  private boolean contains(double target, int coordIdx) {
    double midVal1 = axis.getCoordEdge1(coordIdx);
    double midVal2 = axis.getCoordEdge2(coordIdx);
    if (midVal1 < midVal2)
      return (midVal1 <= target && target <= midVal2);
    else
      return (midVal1 >= target && target >= midVal2);
  }

  // same contract as findCoordElement(); in addition, -1 is returned when the target is not contained in any interval
  // LOOK not using bounded
  private int findCoordElementDiscontiguousInterval(double target, boolean bounded) {
    int idx = findSingleHit(target);
    if (idx >= 0) return idx;
    if (idx == -1) return -1; // no hits

    // multiple hits = choose closest to the midpoint
    return findClosest(target);
  }

  // same contract as findCoordElement(); in addition, -1 is returned when the target is not found
  // LOOK not using bounded
  private int findCoordElementDiscontiguousInterval(double[] target, boolean bounded) {
    for (int i = 0; i < axis.getNcoords(); i++) {
      double edge1 = axis.getCoordEdge1(i);
      double edge2 = axis.getCoordEdge2(i);
      if (Misc.nearlyEquals(edge1, target[0]) && Misc.nearlyEquals(edge2, target[1]))
        return i;
    }
    return -1;
  }

  // return index if only one match, if no matches return -1, if > 1 match return -nhits
  private int findSingleHit(double target) {
    int hits = 0;
    int idxFound = -1;
    int n = axis.getNcoords();
    for (int i = 0; i < n; i++) {
      if (contains(target, i)) {
        hits++;
        idxFound = i;
      }
    }
    if (hits == 1) return idxFound;
    if (hits == 0) return -1;
    return -hits;
  }

  // return index of closest value to target
  // if its a tie, use the larger one
  private int findClosest(double target) {
    double minDiff = Double.MAX_VALUE;
    double useValue = Double.MIN_VALUE;
    int idxFound = -1;
    for (int i = 0; i < axis.getNcoords(); i++) {
      double coord = axis.getCoordMidpoint(i);
      double diff = Math.abs(coord - target);
      if (diff < minDiff || (diff == minDiff && coord > useValue)) {
        minDiff = diff;
        idxFound = i;
        useValue = coord;
      }
    }
    return idxFound;
  }

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

  public Optional<CoverageCoordAxisBuilder> subset(double minValue, double maxValue, int stride) {
    return subsetValues(minValue, maxValue, stride);
  }

  @Nonnull
  public CoverageCoordAxisBuilder subsetClosest(double want) {
    return subsetValuesClosest(want);
  }

  @Nonnull
  public CoverageCoordAxisBuilder subsetLatest() {
    return subsetValuesLatest();
  }

  @Nonnull
  public CoverageCoordAxisBuilder subsetClosest(CalendarDate date) {
    double want = axis.convert(date);
    return subsetValuesClosest(want);
  }

  @Nonnull
  public CoverageCoordAxisBuilder subsetClosest(CalendarDate[] date) {
    double[] want = new double[2];
    want[0] = axis.convert(date[0]);
    want[1] = axis.convert(date[1]);
    return subsetValuesClosest(want);
  }

  public Optional<CoverageCoordAxisBuilder> subset(CalendarDateRange dateRange, int stride) {
    double min = axis.convert(dateRange.getStart());
    double max = axis.convert(dateRange.getEnd());
    return subsetValues(min, max, stride);
  }

  // look could specialize when only one point
  // look must handle discon interval different
  private Optional<CoverageCoordAxisBuilder> subsetValues(double minValue, double maxValue, int stride) {
    if (axis.getSpacing() == CoverageCoordAxis.Spacing.discontiguousInterval)
      return subsetValuesDiscontinuous(minValue, maxValue, stride);

    double lower = axis.isAscending() ? Math.min(minValue, maxValue) : Math.max(minValue, maxValue);
    double upper = axis.isAscending() ? Math.max(minValue, maxValue) : Math.min(minValue, maxValue);

    int minIndex = findCoordElement(lower, false);
    int maxIndex = findCoordElement(upper, false);

    if (minIndex >= axis.getNcoords())
      return Optional.empty(String.format("no points in subset: lower %f > end %f", lower, axis.getEndValue()));
    if (maxIndex < 0)
      return Optional.empty(String.format("no points in subset: upper %f < start %f", upper, axis.getStartValue()));

    if (minIndex < 0)
      minIndex = 0;
    if (maxIndex >= axis.getNcoords())
      maxIndex = axis.getNcoords() - 1;

    int count = maxIndex - minIndex + 1;
    if (count <= 0)
      throw new IllegalArgumentException("no points in subset");

    try {
      return Optional.of(subsetByIndex(new Range(minIndex, maxIndex, stride)));
    } catch (InvalidRangeException e) {
      return Optional.empty(e.getMessage());
    }
  }

  public Optional<RangeIterator> makeRange(double minValue, double maxValue, int stride) {
    //if (axis.getSpacing() == CoverageCoordAxis.Spacing.discontiguousInterval)
    //  return subsetValuesDiscontinuous(minValue, maxValue, stride);

    double lower = axis.isAscending() ? Math.min(minValue, maxValue) : Math.max(minValue, maxValue);
    double upper = axis.isAscending() ? Math.max(minValue, maxValue) : Math.min(minValue, maxValue);

    int minIndex = findCoordElement(lower, false);
    int maxIndex = findCoordElement(upper, false);

    if (minIndex >= axis.getNcoords())
      return Optional.empty(String.format("no points in subset: lower %f > end %f", lower, axis.getEndValue()));
    if (maxIndex < 0)
      return Optional.empty(String.format("no points in subset: upper %f < start %f", upper, axis.getStartValue()));

    if (minIndex < 0)
      minIndex = 0;
    if (maxIndex >= axis.getNcoords())
      maxIndex = axis.getNcoords() - 1;

    int count = maxIndex - minIndex + 1;
    if (count <= 0)
      return Optional.empty("no points in subset");

    try {
      return Optional.of(new Range(minIndex, maxIndex, stride));
    } catch (InvalidRangeException e) {
      return Optional.empty(e.getMessage());
    }
  }

  private Optional<CoverageCoordAxisBuilder> subsetValuesDiscontinuous(double minValue, double maxValue, int stride) {
    return Optional.empty("subsetValuesDiscontinuous not done yet"); // LOOK
  }

  // Range must be contained in this range
  @Nonnull
  CoverageCoordAxisBuilder subsetByIndex(Range range) throws InvalidRangeException {
    int ncoords = range.length();
    if (range.last() >= axis.getNcoords())
      throw new InvalidRangeException("range.last() >= axis.getNcoords()");

    double resolution = 0.0;

    int count2 = 0;
    double[] values = axis.getValues();  // will be null for regular
    double[] subsetValues = null;
    switch (axis.getSpacing()) {
      case regularInterval:
      case regularPoint:
        resolution = range.stride() * axis.getResolution();
        break;

      case irregularPoint:
        subsetValues = new double[ncoords];
        for (int i : range)
          subsetValues[count2++] = values[i];
        break;

      case contiguousInterval:
        subsetValues = new double[ncoords + 1];            // need npts+1
        for (int i : range)
          subsetValues[count2++] = values[i];
        subsetValues[count2] = values[range.last() + 1];
        break;

      case discontiguousInterval:
        subsetValues = new double[2 * ncoords];            // need 2*npts
        for (int i : range) {
          subsetValues[count2++] = values[2 * i];
          subsetValues[count2++] = values[2 * i + 1];
        }
        break;
    }

    // subset(int ncoords, double start, double end, double[] values)
    CoverageCoordAxisBuilder builder = new CoverageCoordAxisBuilder(axis);
    builder.subset(ncoords, axis.getCoordMidpoint(range.first()), axis.getCoordMidpoint(range.last()), resolution, subsetValues);
    builder.setRange(range);
    return builder;
  }

  @Nonnull
  private CoverageCoordAxisBuilder subsetValuesClosest(double[] want) {
    int closest_index = findCoordElement(want, true); // bounded, always valid index
    if (closest_index < 0)
      findCoordElement(want, true);
    CoverageCoordAxisBuilder builder = new CoverageCoordAxisBuilder(axis);

    if (axis.spacing == CoverageCoordAxis.Spacing.regularInterval) {
      double val1 = axis.getCoordEdge1(closest_index);
      double val2 = axis.getCoordEdge2(closest_index);
      builder.subset(1, val1, val2, val2-val1, null);

    } else {
      builder.subset(1, 0, 0, 0.0, makeValues(closest_index));
    }

    try {
      builder.setRange(new Range(closest_index, closest_index));
    } catch (InvalidRangeException e) {
      throw new RuntimeException(e); // cant happen
    }
    return builder;
  }

  @Nonnull
  private CoverageCoordAxisBuilder subsetValuesClosest(double want) {
    int closest_index = findCoordElement(want, true); // bounded, always valid index
    CoverageCoordAxisBuilder builder = new CoverageCoordAxisBuilder(axis);

    if (axis.spacing == CoverageCoordAxis.Spacing.regularPoint) {
      double val = axis.getCoordMidpoint(closest_index);
      builder.subset(1, val, val, 0.0, null);

    } else if (axis.spacing == CoverageCoordAxis.Spacing.regularInterval) {
      double val1 = axis.getCoordEdge1(closest_index);
      double val2 = axis.getCoordEdge2(closest_index);
      builder.subset(1, val1, val2, val2-val1, null);

    } else {
      builder.subset(1, 0, 0, 0.0, makeValues(closest_index));
    }

    try {
      builder.setRange(new Range(closest_index, closest_index));
    } catch (InvalidRangeException e) {
      throw new RuntimeException(e); // cant happen
    }
    return builder;
  }

  Optional<CoverageCoordAxisBuilder> subsetContaining(double want) {
    int index = findCoordElement(want, false); // not bounded, may not be valid index
    if (index < 0 || index >= axis.getNcoords())
      return Optional.empty(String.format("value %f not in axis %s", want, axis.getName()));

    double val = axis.getCoordMidpoint(index);

    CoverageCoordAxisBuilder builder = new CoverageCoordAxisBuilder(axis);
    builder.subset(1, val, val, 0.0, makeValues(index));
    try {
      builder.setRange(new Range(index, index));
    } catch (InvalidRangeException e) {
      throw new RuntimeException(e); // cant happen
    }
    return Optional.of(builder);
  }

  @Nonnull
  private CoverageCoordAxisBuilder subsetValuesLatest() {
    int last = axis.getNcoords() - 1;
    double val = axis.getCoordMidpoint(last);

    CoverageCoordAxisBuilder builder = new CoverageCoordAxisBuilder(axis);
    builder.subset(1, val, val, 0.0, makeValues(last));
    try {
      builder.setRange(new Range(last, last));
    } catch (InvalidRangeException e) {
      throw new RuntimeException(e); // cant happen
    }
    return builder;
  }

  private double[] makeValues(int index) {
    double[] subsetValues = null; // null for regular

    switch (axis.getSpacing()) {
      case irregularPoint:
        subsetValues = new double[1];
        subsetValues[0] = axis.getCoordMidpoint(index);
        break;

      case discontiguousInterval:
      case contiguousInterval:
        subsetValues = new double[2];
        subsetValues[0] = axis.getCoordEdge1(index);
        subsetValues[1] = axis.getCoordEdge2(index);
        break;
    }
    return subsetValues;
  }

  int search(double want) {
    if (axis.getNcoords() == 1) {
      return Misc.nearlyEquals(want, axis.getStartValue()) ? 0 : -1;
    }
    if (axis.isRegular()) {
      double fval = (want - axis.getStartValue()) / axis.getResolution();
      double ival = Math.rint(fval);
      return Misc.nearlyEquals(fval, ival) ? (int) ival : (int) -ival - 1; // LOOK
    }

    // otherwise do a binary search
    return Arrays.binarySearch(axis.getValues(), want);
  }
}


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

package thredds.server.config;

import org.slf4j.Logger;
import org.slf4j.MDC;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import thredds.client.catalog.tools.CatalogXmlWriter;
import thredds.client.catalog.tools.DataFactory;
import thredds.core.AllowedServices;
import thredds.core.ConfigCatalogInitialization;
import thredds.core.DatasetManager;
import thredds.core.StandardService;
import thredds.featurecollection.CollectionUpdater;
import thredds.featurecollection.InvDatasetFeatureCollection;
import thredds.server.catalog.ConfigCatalogCache;
import thredds.server.catalog.DatasetScan;
import thredds.server.ncss.controller.NcssDiskCache;
import thredds.server.ncss.format.FormatsAvailabilityService;
import thredds.server.ncss.format.SupportedFormat;
import thredds.server.notebook.JupyterNotebookServiceCache;
import ucar.nc2.NetcdfFile;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.grib.GribIndexCache;
import ucar.nc2.grib.collection.GribCdmIndex;
import ucar.nc2.jni.netcdf.Nc4Iosp;
import ucar.nc2.ncml.Aggregation;
import ucar.nc2.stream.CdmRemote;
import ucar.nc2.util.DebugFlags;
import ucar.nc2.util.DebugFlagsImpl;
import ucar.nc2.util.DiskCache;
import ucar.nc2.util.DiskCache2;
import ucar.nc2.util.cache.FileCache;
import ucar.unidata.io.RandomAccessFile;
import ucar.util.prefs.PreferencesExt;
import ucar.util.prefs.XMLStore;

import java.io.File;
import java.io.IOException;
import java.util.Calendar;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutorService;

/**
 * A Singleton class to initialize and shutdown the CDM/TDS
 * Formerly CdmInit
 *
 * @author caron
 * @since Feb 20, 2009
 */

@Component
public class TdsInit implements ApplicationListener<ContextRefreshedEvent>, DisposableBean {
  static private final Logger startupLog = org.slf4j.LoggerFactory.getLogger("serverStartup");
  static private final Logger logCatalogInit = org.slf4j.LoggerFactory.getLogger("catalogInit");

  @Autowired
  private TdsContext tdsContext;

  @Autowired
  TdsConfigMapper tdsConfigMapper;

  @Autowired
  private TdsUpdateConfigBean tdsUpdateConfig;

  @Autowired
  private DatasetManager datasetManager;

  @Autowired
  private ConfigCatalogCache ccc;

  @Autowired
  private ConfigCatalogInitialization configCatalogInitializer;

  @Autowired
  CollectionUpdater collectionUpdater;

  @Autowired
  @Qualifier("fcTriggerExecutor")
  private ExecutorService executor;  // need this so we can shut it down

  @Autowired
  private AllowedServices allowedServices;

  @Autowired
  private JupyterNotebookServiceCache jupyterNotebooks;

  @Autowired
  private NcssDiskCache ncssDiskCache;

  private Timer cdmDiskCacheTimer;
  private boolean wasInitialized;

  private XMLStore store;
  private PreferencesExt mainPrefs;

  @Override
  public void onApplicationEvent(ContextRefreshedEvent event) {
    if (event != null) {  // startup
      synchronized (this) {
        if (!wasInitialized) {
          wasInitialized = true;
          startupLog.info("TdsInit: {}", event);
          startupLog.info("TdsInit: getContentRootPathAbsolute= " + tdsContext.getContentRootPathProperty());

          // set debug flags passed into the JVM, as long as flags are actually passed in
          String tdsDebugFlags = tdsContext.getTdsDebugFlags();
          if (!tdsDebugFlags.isEmpty()) {
            startupLog.info(String.format("Setting the following debug flags: %s", tdsDebugFlags));
            setDebugFlags(new DebugFlagsImpl(tdsDebugFlags));
          }

          readState();
          initThreddsConfig();
          readThreddsConfig();
          logVersionMessage();
          // add warning about web start deprecation
          startupLog.warn("Web Start has been deprecated for >Java 9 and is slated for removal - see http://www.oracle.com/technetwork/java/eol-135779.html)");

          // read catalogs
          String readModeS = ThreddsConfig.get("ConfigCatalog.reread", "always");
          ConfigCatalogInitialization.ReadMode readMode =  ConfigCatalogInitialization.ReadMode.get(readModeS);
          if (readMode == null) readMode = ConfigCatalogInitialization.ReadMode.always;
          configCatalogInitializer.init(readMode, (PreferencesExt) mainPrefs.node("configCatalog"));
          startupLog.info("TdsInit complete");
        }
      }
    }
  }

  private void setDebugFlags(DebugFlags debugFlags) {
    NetcdfFile.setDebugFlags(debugFlags);
    ucar.nc2.iosp.hdf5.H5iosp.setDebugFlags(debugFlags);
    ucar.nc2.ncml.NcMLReader.setDebugFlags(debugFlags);
    ucar.nc2.dods.DODSNetcdfFile.setDebugFlags(debugFlags);
    CdmRemote.setDebugFlags(debugFlags);
    Nc4Iosp.setDebugFlags(debugFlags);
    DataFactory.setDebugFlags(debugFlags);

    ucar.nc2.FileWriter2.setDebugFlags(debugFlags);
    ucar.nc2.ft.point.standard.PointDatasetStandardFactory.setDebugFlags(debugFlags);
    ucar.nc2.grib.collection.Grib.setDebugFlags(debugFlags);
  }

  private void readState() {
    File prefsDir = new File(tdsContext.getThreddsDirectory(), "/state/");
    if (!prefsDir.exists()) {
      boolean ok = prefsDir.mkdirs();
      startupLog.info("TdsInit: makeDir= " + prefsDir.getAbsolutePath() + " ok=" + ok);
    }

    File prefsFile = new File(prefsDir, "prefs.xml");
    try {
      store = XMLStore.createFromFile(prefsFile.getAbsolutePath(), null);
      mainPrefs = store.getPreferences();
    } catch (IOException e) {
      startupLog.error("TdsInit: failed to get prefs file= " + prefsDir.getAbsolutePath(), e);
    }
  }

  private void initThreddsConfig() {
    // read in persistent user-defined params from threddsConfig.xml
    File tdsConfigFile = new File(tdsContext.getThreddsDirectory(), tdsContext.getConfigFileProperty());
    if (!tdsConfigFile.exists()) {
      startupLog.warn("TdsInit: TDS configuration file '{}' doesn't exist, using all defaults ", tdsConfigFile
              .getAbsolutePath());
      return;
    }
    ThreddsConfig.init(tdsConfigFile.getPath());
  }

  private void logVersionMessage() {

    // log current server version in catalogInit, where it is most likely to be seen by the user
    String version = tdsContext.getVersionInfo();
    String message = "You are currently running TDS version " + version;
    logCatalogInit.info(message);

    // check and log the latest stable and development version information
    //  only if it is OK according to the threddsConfig file.
    if (tdsUpdateConfig.isLogVersionInfo()) {
      Map<String, String> latestVersionInfo = tdsUpdateConfig.getLatestVersionInfo(version);
      if (!latestVersionInfo.isEmpty()) {
        logCatalogInit.info("Latest Available TDS Version Info:");
        for (Map.Entry entry : latestVersionInfo.entrySet()) {
          message = "latest " + entry.getKey() + " version = " + entry.getValue();
          startupLog.info("TdsInit: " + message);
          logCatalogInit.info("    " + message);
        }
        logCatalogInit.info("");
      }
    }
  }

  private void readThreddsConfig() {
    // initialize the tds configuration beans
    tdsConfigMapper.init(tdsContext);

    // prefer cdmRemote when available
    DataFactory.setPreferCdm(true);
    // netcdf-3 files can only grow, not have metadata changes
    ucar.nc2.NetcdfFile.setProperty("syncExtendOnly", "true");

    // Global config
    boolean useBytesForDataSize = ThreddsConfig.getBoolean("catalogWriting.useBytesForDataSize", false);
    CatalogXmlWriter.useBytesForDataSize(useBytesForDataSize);
    startupLog.info("TdsInit: catalogWriting.useBytesForDataSize= " + useBytesForDataSize);

    // datasetSource plug-in
    ThreddsConfig.getRootList("datasetSource").forEach(datasetManager::registerDatasetSource);

    //////////////////////////////////////////////////////////
    // Controlling Data Services
    // allows users to control services in ThreddsConfig, but not override the default if they havent set
    // LOOK only exposing the ones alreaady in use. maybe let user override tdsGlobalConfig.xml (phase out ThreddsConfig)

    allowedServices.setAllowService(StandardService.catalogRemote, ThreddsConfig.getBoolean("CatalogServices.allowRemote"));
    allowedServices.setAllowService(StandardService.wcs, ThreddsConfig.getBoolean("WCS.allow"));
    allowedServices.setAllowService(StandardService.wms, ThreddsConfig.getBoolean("WMS.allow"));
    allowedServices.setAllowService(StandardService.netcdfSubsetGrid, ThreddsConfig.getBoolean("NetcdfSubsetService.allow"));
    allowedServices.setAllowService(StandardService.netcdfSubsetPoint, ThreddsConfig.getBoolean("NetcdfSubsetService.allow"));
    allowedServices.setAllowService(StandardService.iso_ncml, ThreddsConfig.getBoolean("NCISO.ncmlAllow"));
    allowedServices.setAllowService(StandardService.uddc, ThreddsConfig.getBoolean("NCISO.uddcAllow"));
    allowedServices.setAllowService(StandardService.iso, ThreddsConfig.getBoolean("NCISO.isoAllow"));
    allowedServices.setAllowService(StandardService.jupyterNotebook, ThreddsConfig.getBoolean("JupyterNotebookService.allow"));

    // CDM configuration

    allowedServices.finish(); // finish when we know everything is wired
    InvDatasetFeatureCollection.setAllowedServices(allowedServices);
    DatasetScan.setSpecialServices(allowedServices.getStandardService(StandardService.resolver),
              allowedServices.getStandardService(StandardService.httpServer));
    DatasetScan.setAllowedServices(allowedServices);
    allowedServices.makeDebugActions();

    /*
      <Netcdf4Clibrary>
        <libraryPath>/usr/local/lib</libraryPath>
        <libraryName>netcdf</libraryName>
        <useForReading>false</useForReading>
      </Netcdf4Clibrary>
    */
    String libraryPath = ThreddsConfig.get("Netcdf4Clibrary.libraryPath", null);
    String libraryName = ThreddsConfig.get("Netcdf4Clibrary.libraryName", null);
    if (libraryPath != null || libraryName != null) {
      Nc4Iosp.setLibraryAndPath(libraryPath, libraryName);
    }

    Boolean useForReading = ThreddsConfig.getBoolean("Netcdf4Clibrary.useForReading", false);
    if (useForReading) {
      if (Nc4Iosp.isClibraryPresent()) {
        try {
          // Registers Nc4Iosp in front of all the other IOSPs already registered in NetcdfFile.<clinit>().
          // Crucially, this means that we'll try to open a file with Nc4Iosp before we try it with H5iosp.
          NetcdfFile.registerIOProvider(Nc4Iosp.class);
        } catch (IllegalAccessException | InstantiationException e) {
          startupLog.error("TdsInit: Unable to register IOSP: " + Nc4Iosp.class.getCanonicalName(), e);
        }
      } else {
        startupLog.warn("TdsInit: In threddsConfig.xml, 'Netcdf4Clibrary.useForReading' is 'true' but the native C " +
                "library couldn't be found on the system. Falling back to the pure-Java reader.");
      }
    }

    if (Nc4Iosp.isClibraryPresent()) {  // NetCDF-4 lib could be set as an environment variable or as a JVM parameter.
      FormatsAvailabilityService.setFormatAvailability(SupportedFormat.NETCDF4, true);
      // FormatsAvailabilityService.setFormatAvailability(SupportedFormat.NETCDF4EXT, true);
    }

    // how to choose the typical dataset ?
    String typicalDataset = ThreddsConfig.get("Aggregation.typicalDataset", "penultimate");
    Aggregation.setTypicalDatasetMode(typicalDataset);
    startupLog.info("TdsInit: Aggregation.setTypicalDatasetMode= " + typicalDataset);

    ////////////////////////////////////////////////////////////////
    // Disk Caching
    String dir;
    int scourSecs, maxAgeSecs;

    // Nj22 disk cache
    dir = ThreddsConfig.get("DiskCache.dir", new File(tdsContext.getThreddsDirectory(), "/cache/cdm/").getPath());
    boolean alwaysUse = ThreddsConfig.getBoolean("DiskCache.alwaysUse", false);
    scourSecs = ThreddsConfig.getSeconds("DiskCache.scour", 60 * 60); // default once an hour
    long maxSize = ThreddsConfig.getBytes("DiskCache.maxSize", (long) 1000 * 1000 * 1000);  // default 1 Gbyte
    DiskCache.setRootDirectory(dir);
    DiskCache.setCachePolicy(alwaysUse);
    startupLog.info("TdsInit: CdmCache= " + dir + " scour = " + scourSecs + " maxSize = " + maxSize);
    if (scourSecs > 0) {
      Calendar c = Calendar.getInstance(); // contains current startup time
      c.add(Calendar.SECOND, scourSecs / 2); // starting in half the scour time
      cdmDiskCacheTimer = new Timer("CdmDiskCache");
      cdmDiskCacheTimer.scheduleAtFixedRate(new CacheScourTask(maxSize), c.getTime(), (long) 1000 * scourSecs);
    }

    // persist joinExisting aggregations. default every 24 hours, delete stuff older than 90 days
    dir = ThreddsConfig.get("AggregationCache.dir", new File(tdsContext.getThreddsDirectory().getPath(), "/cache/agg/").getPath());
    scourSecs = ThreddsConfig.getSeconds("AggregationCache.scour", 24 * 60 * 60);
    maxAgeSecs = ThreddsConfig.getSeconds("AggregationCache.maxAge", 90 * 24 * 60 * 60);
    DiskCache2 aggCache = new DiskCache2(dir, false, maxAgeSecs / 60, scourSecs / 60);
    String cachePathPolicy = ThreddsConfig.get("AggregationCache.cachePathPolicy", null);
    aggCache.setPolicy(cachePathPolicy);
    Aggregation.setPersistenceCache(aggCache);
    startupLog.info("TdsInit: AggregationCache= " + dir + " scour = " + scourSecs + " maxAgeSecs = " + maxAgeSecs);

    /* 4.3.15: grib index file placement, using DiskCache2  */
    String gribIndexDir = ThreddsConfig.get("GribIndex.dir", new File(tdsContext.getThreddsDirectory(), "/cache/grib/").getPath());
    Boolean gribIndexAlwaysUse = ThreddsConfig.getBoolean("GribIndex.alwaysUse", false);
    Boolean gribIndexNeverUse = ThreddsConfig.getBoolean("GribIndex.neverUse", false);
    String gribIndexPolicy = ThreddsConfig.get("GribIndex.policy", null);
    int gribIndexScourSecs = ThreddsConfig.getSeconds("GribIndex.scour", 0);
    int gribIndexMaxAgeSecs = ThreddsConfig.getSeconds("GribIndex.maxAge", 90 * 24 * 60 * 60);
    DiskCache2 gribCache = new DiskCache2(gribIndexDir, false, gribIndexMaxAgeSecs / 60, gribIndexScourSecs / 60);
    gribCache.setPolicy(gribIndexPolicy);
    gribCache.setAlwaysUseCache(gribIndexAlwaysUse);
    gribCache.setNeverUseCache(gribIndexNeverUse);
    GribIndexCache.setDiskCache2(gribCache);
    startupLog.info("TdsInit: GribIndex=" + gribCache);

    // LOOK just create the diskCache here and send it in
    ncssDiskCache.init();

    // LOOK is this used ??
    // 4.3.16
    /* dir = ThreddsConfig.get("CdmRemote.dir", new File(tdsContext.getContentDirectory().getPath(), "/cache/cdmr/").getPath());
    scourSecs = ThreddsConfig.getSeconds("CdmRemote.scour", 30 * 60);
    maxAgeSecs = ThreddsConfig.getSeconds("CdmRemote.maxAge", 60 * 60);
    DiskCache2 cdmrCache = new DiskCache2(dir, false, maxAgeSecs / 60, scourSecs / 60);
    CdmrFeatureController.setDiskCache(cdmrCache);
    startupLog.info("TdsInit: CdmRemote= " + dir + " scour = " + scourSecs + " maxAgeSecs = " + maxAgeSecs); */

    // turn back on for 4.6 needed for FMRC
    // turned off for 4.5 not used ??
    // new for 4.2 - feature collection caching
    // in 4.4, change name to FeatureCollectionCache, but keep old for backwards compatibility
    String fcCache = ThreddsConfig.get("FeatureCollectionCache.dir", null);
    if (fcCache == null)
      fcCache = ThreddsConfig.get("FeatureCollection.dir", null);
    if (fcCache == null)
      fcCache = ThreddsConfig.get("FeatureCollection.cacheDirectory", tdsContext.getThreddsDirectory().getPath() + "/cache/collection/");  // cacheDirectory is old way

    long maxSizeBytes = ThreddsConfig.getBytes("FeatureCollectionCache.maxSize", -1);
    if (maxSizeBytes == -1)
      maxSizeBytes = ThreddsConfig.getBytes("FeatureCollection.maxSize", 0);

    int jvmPercent = ThreddsConfig.getInt("FeatureCollectionCache.jvmPercent", -1);
    if (-1 == jvmPercent)
      jvmPercent = ThreddsConfig.getInt("FeatureCollection.jvmPercent", 2);

    try {
      thredds.inventory.bdb.MetadataManager.setCacheDirectory(fcCache, maxSizeBytes, jvmPercent);
      thredds.inventory.CollectionManagerAbstract.setMetadataStore(thredds.inventory.bdb.MetadataManager.getFactory());  // LOOK
      startupLog.info("TdsInit: CollectionManagerAbstract.setMetadataStore= " + fcCache);
    } catch (Exception e) {
      startupLog.error("TdsInit: Failed to open CollectionManagerAbstract.setMetadataStore= " + fcCache, e);
    }

    ///////////////////////////////////////////////
    // Object caching
    int min, max, secs;

    // RandomAccessFile: default is allow 400 - 500 open files, cleanup every 11 minutes
    min = ThreddsConfig.getInt("RandomAccessFile.minFiles", 400);
    max = ThreddsConfig.getInt("RandomAccessFile.maxFiles", 500);
    secs = ThreddsConfig.getSeconds("RandomAccessFile.scour", 11 * 60);
    if (max > 0) {
      RandomAccessFile.setGlobalFileCache(new FileCache("RandomAccessFile", min, max, -1, secs));
      startupLog.info("TdsInit: RandomAccessFile.initPartitionCache= [" + min + "," + max + "] scour = " + secs);
    }

    // NetcdfFileCache : default is allow 100 - 150 open files, cleanup every 12 minutes
    min = ThreddsConfig.getInt("NetcdfFileCache.minFiles", 100);
    max = ThreddsConfig.getInt("NetcdfFileCache.maxFiles", 150);
    secs = ThreddsConfig.getSeconds("NetcdfFileCache.scour", 12 * 60);
    if (max > 0) {
      NetcdfDataset.initNetcdfFileCache(min, max, secs);
      startupLog.info("TdsInit: NetcdfDataset.initNetcdfFileCache= [" + min + "," + max + "] scour = " + secs);
    }

    // GribCollection partitions: default is allow 100 - 150 objects, cleanup every 13 minutes
    min = ThreddsConfig.getInt("TimePartition.minFiles", 100);
    max = ThreddsConfig.getInt("TimePartition.maxFiles", 150);
    secs = ThreddsConfig.getSeconds("TimePartition.scour", 13 * 60);
    if (max > 0) {
      GribCdmIndex.initDefaultCollectionCache(min, max, secs);
      startupLog.info("TdsInit: GribCdmIndex.initDefaultCollectionCache= [" + min + "," + max + "] scour = " + secs);
    }

    //RandomAccessFile.enableDefaultGlobalFileCache();
    //RandomAccessFile.setDebugLeaks(true);

    // Config Cat Cache
    max = ThreddsConfig.getInt("ConfigCatalog.keepInMemory", 100);
    String rootPath = tdsContext.getContentRootPathProperty() + "thredds/";
    ccc.init(rootPath, max);

    // Config Dataset Tracker
    String trackerDir = ThreddsConfig.get("ConfigCatalog.dir", new File(tdsContext.getThreddsDirectory().getPath(), "/cache/catalog/").getPath());
    int trackerMax = ThreddsConfig.getInt("ConfigCatalog.maxDatasets", 10 * 1000);
    File trackerDirFile = new File(trackerDir);
    if (!trackerDirFile.exists()) {
      boolean ok = trackerDirFile.mkdirs();
      startupLog.info("TdsInit: tracker directory {} make ok = {}", trackerDir, ok);
    }
    configCatalogInitializer.setTrackerDir(trackerDir);
    configCatalogInitializer.setMaxDatasetToTrack(trackerMax);

    // Jupyter notebook service cache
    if (allowedServices.isAllowed(StandardService.jupyterNotebook)) {
      max = ThreddsConfig.getInt("JupyterNotebookService.maxFiles", 100);
      secs = ThreddsConfig.getSeconds("JupyterNotebookService.maxAge", 60*60);
      jupyterNotebooks.init(max, secs);
    }
  }

  static private class CacheScourTask extends TimerTask {
    long maxBytes;

    CacheScourTask(long maxBytes) {
      this.maxBytes = maxBytes;
    }

    public void run() {
      StringBuilder sbuff = new StringBuilder();
      DiskCache.cleanCache(maxBytes, sbuff); // 1 Gbyte
      sbuff.append("----------------------\n");
      // cacheLog.info(sbuff.toString());
    }
  }

  /*
  http://stackoverflow.com/questions/24660408/how-can-i-get-intellij-debugger-to-allow-my-apps-shutdown-hooks-to-run?lq=1
  "Unfortunately, you can't use breakpoints in your shutdown hook body when you use Stop button: these breakpoints are silently ignored."
   */
  @Override
  public void destroy() {
    System.out.printf("TdsInit.destroy() is called%n");

    // prefs
    try {
      store.save();
    } catch (IOException ioe) {
      ioe.printStackTrace();
      startupLog.error("TdsInit: Prefs save failed", ioe);
    }

    // background threads
    if (cdmDiskCacheTimer != null)
      cdmDiskCacheTimer.cancel();
    FileCache.shutdown();              // this handles background threads for all instances of FileCache
    DiskCache2.exit();                // this handles background threads for all instances of DiskCache2
    thredds.inventory.bdb.MetadataManager.closeAll();
    executor.shutdownNow();

    /* try {
      catalogWatcher.close();
    } catch (IOException ioe) {
      ioe.printStackTrace();
      startupLog.error("catalogWatcher close failed", ioe);
    } */

    // open file caches
    RandomAccessFile.shutdown();
    NetcdfDataset.shutdown();

    // memory caches
    GribCdmIndex.shutdown();
    datasetManager.setDatasetTracker(null); // closes the existing tracker

    collectionUpdater.shutdown();
    startupLog.info("TdsInit shutdown");
    MDC.clear();
  }

}