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

import com.google.common.base.Preconditions;
import ucar.nc2.util.Misc;
import ucar.unidata.util.Format;

import java.util.Objects;
import java.util.StringTokenizer;

/**
 * Bounding box for latitude/longitude points.
 * This is a rectangle in lat/lon coordinates.
 * This class handles the longitude wrapping problem.
 * The Rectangle always starts from lowerLeft, goes east width degrees until upperRight
 * For latitude, lower < upper.
 * Since the longitude must be in the range +/-180., right may be less or greater than left.
 *
 * @author Russ Rew
 * @author John Caron
 */
public class LatLonRect {

  public static LatLonRect INVALID = new LatLonRect(LatLonPointImmutable.INVALID, LatLonPointImmutable.INVALID);

  /**
   * upper right corner
   */
  private LatLonPointImpl upperRight;

  /**
   * lower left corner
   */
  private LatLonPointImpl lowerLeft;

  /**
   * flag for dateline cross
   */
  private boolean crossDateline = false;

  /**
   * All longitudes are included
   */
  private boolean allLongitude = false;

  /**
   * width and initial longitude
   */
  private double width, lon0;

  /**
   * Construct a lat/lon bounding box from a point, and a delta lat, lon.
   * This disambiguates which way the box wraps around the globe.
   *
   * @param p1       one corner of the box
   * @param deltaLat delta lat from p1. (may be positive or negetive)
   * @param deltaLon delta lon from p1. (may be positive or negetive)
   */
  public LatLonRect(LatLonPoint p1, double deltaLat, double deltaLon) {
    init(p1, deltaLat, deltaLon);
  }

  private void init(LatLonPoint p1, double deltaLat, double deltaLon) {
    double lonmin, lonmax;
    double latmin = Math.min(p1.getLatitude(), p1.getLatitude() + deltaLat);
    double latmax = Math.max(p1.getLatitude(), p1.getLatitude() + deltaLat);

    double lonpt = p1.getLongitude();
    if (deltaLon > 0) {
      lonmin = lonpt;
      lonmax = lonpt + deltaLon;
      crossDateline = (lonmax > 180.0);
    } else {
      lonmax = lonpt;
      lonmin = lonpt + deltaLon;
      crossDateline = (lonmin < -180.0);
    }

    this.lowerLeft = new LatLonPointImpl(latmin, lonmin);
    this.upperRight = new LatLonPointImpl(latmax, lonmax);

    // these are an alternative way to view the longitude range
    this.width = Math.abs(deltaLon);
    this.lon0 = LatLonPointImpl.lonNormal(p1.getLongitude() + deltaLon / 2);
    this.allLongitude = (this.width >= 360.0);
  }

  /**
   * Construct a lat/lon bounding box from two points.
   * The order of longitude coord of the two points matters:
   * pt1.lon is always the "left" point, then points contained within the box
   * increase (unless crossing the Dateline, in which case they jump to -180, but
   * then start increasing again) until pt2.lon
   * The order of lat doesnt matter: smaller will go to "lower" point (further south)
   *
   * @param left  left corner
   * @param right right corner
   */
  public LatLonRect(LatLonPoint left, LatLonPoint right) {
    this(left, right.getLatitude() - left.getLatitude(),
        LatLonPointImpl.lonNormal360(right.getLongitude() - left.getLongitude()));
  }

  /**
   * Construct a lat/lon bounding box from a string.
   * @param spec "lat, lon, deltaLat, deltaLon"
   * @see #LatLonRect(LatLonPoint p1, double deltaLat, double deltaLon)
   */
  public LatLonRect(String spec) {
    StringTokenizer stoker = new StringTokenizer( spec, " ,");
    int n = stoker.countTokens();
    if (n != 4) throw new IllegalArgumentException("Must be 4 numbers = lat, lon, latWidth, lonWidth");
    double lat = Double.parseDouble(stoker.nextToken());
    double lon = Double.parseDouble(stoker.nextToken());
    double deltaLat = Double.parseDouble(stoker.nextToken());
    double deltaLon = Double.parseDouble(stoker.nextToken());

    init(new LatLonPointImpl(lat, lon), deltaLat, deltaLon);
  }

  /**
   * Copy Constructor
   *
   * @param r rectangle to copy
   */
  public LatLonRect(LatLonRect r) {
    this(r.getLowerLeftPoint(), r.getUpperRightPoint().getLatitude() - r.getLowerLeftPoint().getLatitude(), r.getWidth());
  }

  /**
   * Create a LatLonRect that covers the whole world.
   */
  public LatLonRect() {
    this(new LatLonPointImpl(-90, -180), 180, 360);
  }

  /**
   * Get the upper right corner of the bounding box.
   *
   * @return upper right corner of the bounding box
   */
  public LatLonPointImpl getUpperRightPoint() {
    return upperRight;
  }

  /**
   * Get the lower left corner of the bounding box.
   *
   * @return lower left corner of the bounding box
   */
  public LatLonPointImpl getLowerLeftPoint() {
    return lowerLeft;
  }

  /**
   * Get the upper left corner of the bounding box.
   *
   * @return upper left corner of the bounding box
   */
  public LatLonPointImpl getUpperLeftPoint() {
    return new LatLonPointImpl(upperRight.getLatitude(), lowerLeft.getLongitude());
  }

  /**
   * Get the lower left corner of the bounding box.
   *
   * @return lower left corner of the bounding box
   */
  public LatLonPointImpl getLowerRightPoint() {
    return new LatLonPointImpl(lowerLeft.getLatitude(), upperRight.getLongitude());
  }

  /**
   * Get whether the bounding box crosses the +/- 180 seam
   *
   * @return true if the bounding box crosses the +/- 180 seam
   */
  public boolean crossDateline() {
    return crossDateline;
  }

  // Exact comparison is needed in order to be consistent with hashCode().
  @Override public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    LatLonRect that = (LatLonRect) o;
    return Objects.equals(upperRight, that.upperRight) && Objects.equals(lowerLeft, that.lowerLeft);
  }

  @Override public int hashCode() {
    return Objects.hash(upperRight, lowerLeft);
  }

  /**
   * Returns the result of {@link #nearlyEquals(LatLonRect, double)}, with {@link Misc#defaultMaxRelativeDiffFloat}.
   */
  public boolean nearlyEquals(LatLonRect other) {
    return nearlyEquals(other, Misc.defaultMaxRelativeDiffFloat);
  }

  /**
   * Returns {@code true} if this rectangle is nearly equal to {@code other}. The "near equality" of corners is
   * determined using {@link LatLonPoint#nearlyEquals(LatLonPoint, double)}, with the specified maxRelDiff.
   *
   * @param other    the other rectangle to check.
   * @param maxRelDiff  the maximum {@link Misc#relativeDifference relative difference} that two corners may have.
   * @return {@code true} if this rectangle is nearly equal to {@code other}.
   */
  public boolean nearlyEquals(LatLonRect other, double maxRelDiff) {
    return this.getLowerLeftPoint() .nearlyEquals(other.getLowerLeftPoint(),  maxRelDiff) &&
           this.getUpperRightPoint().nearlyEquals(other.getUpperRightPoint(), maxRelDiff);
  }

  /**
   * return width of bounding box, always between 0 and 360 degrees.
   *
   * @return width of bounding box in degrees longitude
   */
  public double getWidth() {
    return width;
  }

  /**
   * return height of bounding box, always between 0 and 180 degrees.
   *
   * @return height of bounding box in degrees latitude
   */
  public double getHeight() {
    return getLatMax() - getLatMin();
  }

  /**
   * return center Longitude, always in the range +/-180
   *
   * @return center Longitude
   */
  public double getCenterLon() {
    return lon0;
  }

  /**
   * Get minimum longitude, aka "west" edge
   *
   * @return minimum longitude
   */
  public double getLonMin() {
    return lowerLeft.getLongitude();
  }

  /**
   * Get maximum longitude, aka "east" edge
   *
   * @return maximum longitude
   */
  public double getLonMax() {
    return lowerLeft.getLongitude() + width;
  }

  /**
   * Get minimum latitude, aka "south" edge
   *
   * @return minimum latitude
   */
  public double getLatMin() {
    return lowerLeft.getLatitude();
  }

  /**
   * Get maximum latitude, aka "north" edge
   *
   * @return maximum latitude
   */
  public double getLatMax() {
    return upperRight.getLatitude();
  }

  /**
   * Determine if a specified LatLonPoint is contained in this bounding box.
   *
   * @param p the specified point to be tested
   * @return true if point is contained in this bounding box
   */
  public boolean contains(LatLonPoint p) {
    return contains(p.getLatitude(), p.getLongitude());
  }

  /**
   * Determine if the given lat/lon point is contined inside this rectangle.
   *
   * @param lat lat of point
   * @param lon lon of point
   * @return true if the given lat/lon point is contined inside this rectangle
   */
  public boolean contains(double lat, double lon) {
    // check lat first
    double eps = 1.0e-9;
    if ((lat + eps < lowerLeft.getLatitude()) || (lat - eps > upperRight.getLatitude())) {
      return false;
    }

    if (allLongitude)
      return true;

    if (crossDateline) {
      // bounding box crosses the +/- 180 seam
      return ((lon >= lowerLeft.getLongitude()) || (lon <= upperRight.getLongitude()));
    } else {
      // check "normal" lon case
      return ((lon >= lowerLeft.getLongitude()) && (lon <= upperRight.getLongitude()));
    }
  }

  /**
   * Determine if this bounding box is contained in another LatLonRect.
   *
   * @param b the other box to see if it contains this one
   * @return true if b contained in this bounding box
   */
  public boolean containedIn(LatLonRect b) {
    return (b.getWidth() >= width) && b.contains(upperRight)
        && b.contains(lowerLeft);
  }

  /*
  * Determine if a specified LatLonRect intersects this
  * @param b the specified box to be tested
  *
  * @param p
  * @return true if b intersects this bounding box
  *
  * public boolean intersects(LatLonRect b) {
  *     if (b.getUpperRightPoint().getLatitude() < lowerLeft.getLatitude())
  * return false;
  *     if (b.getLowerLeftPoint().getLatitude() > upperRight.getLatitude())
  * return false;
  *
  * double blon0 = b.getCenterLon();
  * double normal = (blon0 + lon0) / 2;
  * if (Math.abs(blon0-lon0) > 180.0)
  * normal += 180;
  * blon0 = LatLonPoint.lonNormal(blon0, normal);
  * double mylon0 = LatLonPoint.lonNormal(lon0, normal);
  *
  *     if (blon0 + b.getWidth() < mylon0 - width)
  * return false;
  *     if (blon0 - b.getWidth() > mylon0 + width)
  * return false;
  *
  * return true;
  * }
  */

  /**
   * Extend the bounding box to contain this point
   *
   * @param p point to include
   */
  public void extend(LatLonPoint p) {
    if (contains(p))
      return;

    double lat = p.getLatitude();
    double lon = p.getLongitude();

    // lat is easy to deal with
    if (lat > upperRight.getLatitude()) {
      upperRight.setLatitude(lat);
    }
    if (lat < lowerLeft.getLatitude()) {
      lowerLeft.setLatitude(lat);
    }

    // lon is uglier
    if (allLongitude) {
      // do nothing
    } else if (crossDateline) {

      // bounding box crosses the +/- 180 seam
      double d1 = lon - upperRight.getLongitude();
      double d2 = lowerLeft.getLongitude() - lon;
      if ((d1 > 0.0) && (d2 > 0.0)) {  // needed ?
        if (d1 > d2) {
          lowerLeft.setLongitude(lon);
        } else {
          upperRight.setLongitude(lon);
        }
      }

    } else {

      // normal case
      if (lon > upperRight.getLongitude()) {
        if (lon - upperRight.getLongitude() > lowerLeft.getLongitude() - lon + 360) {
          crossDateline = true;
          lowerLeft.setLongitude(lon);
        } else {
          upperRight.setLongitude(lon);
        }
      } else if (lon < lowerLeft.getLongitude()) {
        if (lowerLeft.getLongitude() - lon > lon + 360.0 - upperRight.getLongitude()) {
          crossDateline = true;
          upperRight.setLongitude(lon);
        } else {
          lowerLeft.setLongitude(lon);
        }
      }
    }

    // recalc delta, center
    width = upperRight.getLongitude() - lowerLeft.getLongitude();
    lon0 = (upperRight.getLongitude() + lowerLeft.getLongitude()) / 2;
    if (crossDateline) {
      width += 360;
      lon0 -= 180;
    }

    this.allLongitude = this.allLongitude || (this.width >= 360.0);
  }

  /**
   * Extend the bounding box to contain the given rectangle
   *
   * @param r rectangle to include
   */
  public void extend(LatLonRect r) {
    Preconditions.checkNotNull(r);

    // lat is easy
    double latMin = r.getLatMin();
    double latMax = r.getLatMax();

    if (latMax > upperRight.getLatitude()) {
      upperRight.setLatitude(latMax);
    }
    if (latMin < lowerLeft.getLatitude()) {
      lowerLeft.setLatitude(latMin);
    }

    // lon is uglier
    if (allLongitude)
      return;

    // everything is reletive to current LonMin
    double lonMin = getLonMin();
    double lonMax = getLonMax();

    double nlonMin = LatLonPointImpl.lonNormal( r.getLonMin(), lonMin);
    double nlonMax = nlonMin + r.getWidth();
    lonMin = Math.min(lonMin, nlonMin);
    lonMax = Math.max(lonMax, nlonMax);

    width = lonMax - lonMin;
    allLongitude = width >= 360.0;
    if (allLongitude) {
      width = 360.0;
      lonMin = -180.0;
    } else {
      lonMin = LatLonPointImpl.lonNormal(lonMin);
    }

    lowerLeft.setLongitude(lonMin);
    upperRight.setLongitude(lonMin+width);
    lon0 = lonMin+width/2;
    crossDateline = lowerLeft.getLongitude() > upperRight.getLongitude();
  }

  /**
   * Create the instersection of this LatLon with the given one
   *
   * @param clip intersect with this
   * @return intersection, or null if there is no intersection
   */
  public LatLonRect intersect(LatLonRect clip) {
    double latMin = Math.max(getLatMin(), clip.getLatMin());
    double latMax = Math.min(getLatMax(), clip.getLatMax());
    double deltaLat = latMax - latMin;
    if (deltaLat < 0)
      return null;

    // lon as always is a pain : if not intersection, try +/- 360
    double lon1min = getLonMin();
    double lon1max = getLonMax();
    double lon2min = clip.getLonMin();
    double lon2max = clip.getLonMax();
    if (!intersect(lon1min, lon1max, lon2min, lon2max)) {
      lon2min = clip.getLonMin() + 360;
      lon2max = clip.getLonMax() + 360;
      if (!intersect(lon1min, lon1max, lon2min, lon2max)) {
        lon2min = clip.getLonMin() - 360;
        lon2max = clip.getLonMax() - 360;
      }
    }

    // we did our best to find an intersection
    double lonMin = Math.max(lon1min, lon2min);
    double lonMax = Math.min(lon1max, lon2max);
    double deltaLon = lonMax - lonMin;
    if (deltaLon < 0)
      return null;

    return new LatLonRect(new LatLonPointImpl(latMin, lonMin), deltaLat, deltaLon);
  }

  private boolean intersect(double min1, double max1, double min2, double max2) {
    double min = Math.max(min1, min2);
    double max = Math.min(max1, max2);
    return min < max;
  }

  /**
   * Return a String representation of this object.
   *
eg: ll: 90.0S .0E+ ur: 90.0N .0E

   *
   * @return a String representation of this object.
   */
  public String toString() {
    return " ll: " + lowerLeft + "+ ur: " + upperRight;
  }

  /**
   * Return a String representation of this object.
   *
lat= [-90.00,90.00] lon= [0.00,360.00

   *
   * @return a String representation of this object.
   */
  public String toString2() {
    return " lat= [" + Format.dfrac(getLatMin(), 2) + "," + Format.dfrac(getLatMax(), 2) +
        "] lon= [" + Format.dfrac(getLonMin(), 2) + "," + Format.dfrac(getLonMax(), 2) + "]";
  }
}


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

import java.math.BigInteger;
import java.util.Arrays;
import java.util.Objects;
import ucar.nc2.util.Misc;

/**
 * Element by element algebra on Arrays
 *
 * @author caron
 * @see Index
 */
public class MAMath {

  /**
   * Add elements of two arrays together, allocating the result array.
   * The result type and the operation type are taken from the type of a.
   *
   * @param a add values from here
   * @param b add values from here
   * @return result = a + b
   * @throws IllegalArgumentException      a and b are not conformable
   * @throws UnsupportedOperationException dont support this data type yet
   */
  public static Array add(Array a, Array b) throws IllegalArgumentException {

    Array result = Array.factory(a.getDataType(), a.getShape());

    if (a.getElementType() == double.class) {
      addDouble(result, a, b);
    } else
      throw new UnsupportedOperationException();

    return result;
  }

  /**
   * Add elements of two arrays together as doubles, place sum in the result array.
   * The values from the arrays a and b are converted to double (if needed),
   * and the sum is converted to the type of result (if needed).
   *
   * @param result result array
   * @param a operand
   * @param b operand
   * @throws IllegalArgumentException a,b,and result are not conformable
   */
  public static void addDouble(Array result, Array a, Array b)
      throws IllegalArgumentException {

    if (!conformable(result, a) || !conformable(a, b))
      throw new IllegalArgumentException();

    IndexIterator iterR = result.getIndexIterator();
    IndexIterator iterA = a.getIndexIterator();
    IndexIterator iterB = b.getIndexIterator();

    while (iterA.hasNext())
      iterR.setDoubleNext(iterA.getDoubleNext() + iterB.getDoubleNext());
  }

  /**
   * Check that two arrays are conformable.
   *
   * @param a operand
   * @param b operand
   * @return true if conformable
   */
  public static boolean conformable(Array a, Array b) {
    return conformable(a.getShape(), b.getShape());
  }

  /**
   * Check that two array shapes are conformable.
   * The shapes must match exactly, except that dimensions of length 1 are ignored.
   *
   * @param shapeA shape of array 1
   * @param shapeB shape of array 2
   * @return true if conformable
   */
  public static boolean conformable(int[] shapeA, int[] shapeB) {
    if (reducedRank(shapeA) != reducedRank(shapeB))
      return false;

    int rankB = shapeB.length;

    int dimB = 0;
    for (int aShapeA : shapeA) {
      //System.out.println(dimA + " "+ dimB);

      //skip length 1 dimensions
      if (aShapeA == 1)
        continue;
      while (dimB < rankB)
        if (shapeB[dimB] == 1) dimB++;
        else break;

      // test same shape (NB dimB cant be > rankB due to first test)
      if (aShapeA != shapeB[dimB])
        return false;
      dimB++;
    }

    return true;
  }

  /**
   * Convert original array to desired type
   *
   * @param org original array
   * @param wantType desired type
   * @return converted data of desired type, or original array if it is already
   */
  public static Array convert( Array org, DataType wantType) {
    if (org == null) return null;
    Class wantClass = wantType.getPrimitiveClassType();
    if (org.getElementType().equals(wantClass))
      return org;

    Array result = Array.factory(wantType, org.getShape());
    copy(wantType, org.getIndexIterator(), result.getIndexIterator());
    return result;
  }

  /**
   * Copy using iterators. Will copy until !from.hasNext().
   *
   * @param dataType use this operation type (eg DataType.DOUBLE uses getDoubleNext())
   * @param from     copy from here
   * @param to       copy to here
   * @throws IllegalArgumentException      a and b are not conformable
   * @throws UnsupportedOperationException dont support this data type
   */
  public static void copy(DataType dataType, IndexIterator from, IndexIterator to) throws IllegalArgumentException {
    if (dataType == DataType.DOUBLE) {
      while (from.hasNext())
        to.setDoubleNext(from.getDoubleNext());
    } else if (dataType == DataType.FLOAT) {
      while (from.hasNext())
        to.setFloatNext(from.getFloatNext());
    } else if (dataType.getPrimitiveClassType() == long.class) {
      while (from.hasNext())
        to.setLongNext(from.getLongNext());
    } else if (dataType.getPrimitiveClassType() == int.class) {
      while (from.hasNext())
        to.setIntNext(from.getIntNext());
    } else if (dataType.getPrimitiveClassType() == short.class) {
      while (from.hasNext())
        to.setShortNext(from.getShortNext());
    } else if (dataType == DataType.CHAR) {
      while (from.hasNext())
        to.setCharNext(from.getCharNext());
    } else if (dataType.getPrimitiveClassType() == byte.class) {
      while (from.hasNext())
        to.setByteNext(from.getByteNext());
    } else if (dataType == DataType.BOOLEAN) {
      while (from.hasNext())
        to.setBooleanNext(from.getBooleanNext());
    } else {
      while (from.hasNext())
        to.setObjectNext(from.getObjectNext());
    }
  }

  /**
   * Copy array a to array result, the result array will be in canonical order
   * The operation type is taken from the type of a.
   *
   * @param result copy to here
   * @param a copy from here
   *
   * @throws IllegalArgumentException      a and b are not conformable
   * @throws UnsupportedOperationException dont support this data type yet
   */
  public static void copy(Array result, Array a) throws IllegalArgumentException {
    Class classType = a.getElementType();
    if (classType == double.class) {
      copyDouble(result, a);
    } else if (classType == float.class) {
      copyFloat(result, a);
    } else if (classType == long.class) {
      copyLong(result, a);
    } else if (classType == int.class) {
      copyInt(result, a);
    } else if (classType == short.class) {
      copyShort(result, a);
    } else if (classType == char.class) {
      copyChar(result, a);
    } else if (classType == byte.class) {
      copyByte(result, a);
    } else if (classType == boolean.class) {
      copyBoolean(result, a);
    } else
      copyObject(result, a);
  }

  /**
   * copy array a to array result as doubles
   * The values from the arrays a are converted to double (if needed),
   * and then converted to the type of result (if needed).
   *
   * @param result copy to here
   * @param a copy from here
   *
   * @throws IllegalArgumentException a and result are not conformable
   */
  public static void copyDouble(Array result, Array a) throws IllegalArgumentException {
    if (!conformable(a, result))
      throw new IllegalArgumentException("copy arrays are not conformable");

    IndexIterator iterA = a.getIndexIterator();
    IndexIterator iterR = result.getIndexIterator();
    while (iterA.hasNext())
      iterR.setDoubleNext(iterA.getDoubleNext());
  }

  /**
   * copy array a to array result as floats
   * The values from the arrays a are converted to float (if needed),
   * and then converted to the type of result (if needed).
   *
   * @param result copy to here
   * @param a copy from here
   *
   * @throws IllegalArgumentException a and result are not conformable
   */
  public static void copyFloat(Array result, Array a) throws IllegalArgumentException {
    if (!conformable(a, result))
      throw new IllegalArgumentException("copy arrays are not conformable");

    IndexIterator iterA = a.getIndexIterator();
    IndexIterator iterR = result.getIndexIterator();
    while (iterA.hasNext())
      iterR.setFloatNext(iterA.getFloatNext());
  }

  /**
   * copy array a to array result as longs
   * The values from the array a are converted to long (if needed),
   * and then converted to the type of result (if needed).
   * @param result copy to here
   * @param a copy from here
   *
   *
   * @throws IllegalArgumentException a and result are not conformable
   */
  public static void copyLong(Array result, Array a) throws IllegalArgumentException {
    if (!conformable(a, result))
      throw new IllegalArgumentException("copy arrays are not conformable");

    IndexIterator iterA = a.getIndexIterator();
    IndexIterator iterR = result.getIndexIterator();
    while (iterA.hasNext())
      iterR.setLongNext(iterA.getLongNext());
  }

  /**
   * copy array a to array result as integers
   * The values from the arrays a are converted to integer (if needed),
   * and then converted to the type of result (if needed).
   *
   * @param result copy to here
   * @param a copy from here
   *
   * @throws IllegalArgumentException a and result are not conformable
   */
  public static void copyInt(Array result, Array a) throws IllegalArgumentException {
    if (!conformable(a, result))
      throw new IllegalArgumentException("copy arrays are not conformable");

    IndexIterator iterA = a.getIndexIterator();
    IndexIterator iterR = result.getIndexIterator();
    while (iterA.hasNext())
      iterR.setIntNext(iterA.getIntNext());
  }

  /**
   * copy array a to array result as shorts
   * The values from the array a are converted to short (if needed),
   * and then converted to the type of result (if needed).
   *
   * @param result copy to here
   * @param a copy from here
   *
   * @throws IllegalArgumentException a and result are not conformable
   */
  public static void copyShort(Array result, Array a) throws IllegalArgumentException {
    if (!conformable(a, result))
      throw new IllegalArgumentException("copy arrays are not conformable");

    IndexIterator iterA = a.getIndexIterator();
    IndexIterator iterR = result.getIndexIterator();
    while (iterA.hasNext())
      iterR.setShortNext(iterA.getShortNext());
  }

  /**
   * copy array a to array result as char
   * The values from the array a are converted to char (if needed),
   * and then converted to the type of result (if needed).
   *
   * @param result copy to here
   * @param a copy from here
   *
   * @throws IllegalArgumentException a and result are not conformable
   */
  public static void copyChar(Array result, Array a) throws IllegalArgumentException {
    if (!conformable(a, result))
      throw new IllegalArgumentException("copy arrays are not conformable");

    IndexIterator iterA = a.getIndexIterator();
    IndexIterator iterR = result.getIndexIterator();
    while (iterA.hasNext())
      iterR.setCharNext(iterA.getCharNext());
  }

  /**
   * copy array a to array result as bytes
   * The values from the array a are converted to byte (if needed),
   * and then converted to the type of result (if needed).
   *
   * @param result copy to here
   * @param a copy from here
   *
   * @throws IllegalArgumentException a and result are not conformable
   */
  public static void copyByte(Array result, Array a) throws IllegalArgumentException {
    if (!conformable(a, result))
      throw new IllegalArgumentException("copy arrays are not conformable");

    IndexIterator iterA = a.getIndexIterator();
    IndexIterator iterR = result.getIndexIterator();
    while (iterA.hasNext())
      iterR.setByteNext(iterA.getByteNext());
  }

  /**
   * copy array a to array result as bytes
   * The array a and result must be type boolean
   *
   * @param result copy to here
   * @param a copy from here
   *
   * @throws IllegalArgumentException a and result are not conformable
   */
  public static void copyBoolean(Array result, Array a) throws IllegalArgumentException {
    if (!conformable(a, result))
      throw new IllegalArgumentException("copy arrays are not conformable");

    IndexIterator iterA = a.getIndexIterator();
    IndexIterator iterR = result.getIndexIterator();
    while (iterA.hasNext())
      iterR.setBooleanNext(iterA.getBooleanNext());
  }

  /**
   * copy array a to array result as an Object
   * The array a and result must be type object
   *
   * @param result copy to here
   * @param a copy from here
   *
   * @throws IllegalArgumentException a and result are not conformable
   */
  public static void copyObject(Array result, Array a) throws IllegalArgumentException {
    if (!conformable(a, result))
      throw new IllegalArgumentException("copy arrays are not conformable");

    IndexIterator iterA = a.getIndexIterator();
    IndexIterator iterR = result.getIndexIterator();
    while (iterA.hasNext()) {
      iterR.setObjectNext( iterA.getObjectNext());
    }
  }

  /**
   * Calculate the reduced rank of this shape, by subtracting dimensions with length 1
   * @param shape shape of the array
   * @return rank without dimensions of length 1
   */
  public static int reducedRank(int[] shape) {
    int rank = 0;
    for (int aShape : shape) {
      if (aShape > 1)
        rank++;
    }
    return rank;
  }

  public static double getMinimum(Array a) {
    IndexIterator iter = a.getIndexIterator();
    double min = Double.MAX_VALUE;
    while (iter.hasNext()) {
      double val = iter.getDoubleNext();
      if (Double.isNaN(val)) continue;
      if (val < min)
        min = val;
    }
    return min;
  }

  public static double getMaximum(Array a) {
    IndexIterator iter = a.getIndexIterator();
    double max = -Double.MAX_VALUE;
    while (iter.hasNext()) {
      double val = iter.getDoubleNext();
      if (Double.isNaN(val)) continue;
      if (val > max)
        max = val;
    }
    return max;
  }

  /**
   * Find min and max value in this array, getting values as doubles. Skip Double.NaN.
   *
   * @param a the array.
   * @return MinMax
   */
  public static MAMath.MinMax getMinMax(Array a) {
    IndexIterator iter = a.getIndexIterator();
    double max = -Double.MAX_VALUE;
    double min = Double.MAX_VALUE;
    while (iter.hasNext()) {
      double val = iter.getDoubleNext();
      if (Double.isNaN(val)) continue;
      if (val > max)
        max = val;
      if (val < min)
        min = val;
    }
    return new MinMax(min, max);
  }

  public static MAMath.MinMax getMinMaxSkipMissingData(Array a, IsMissingEvaluator eval) {
    if (eval == null || !eval.hasMissing())
      return MAMath.getMinMax(a);

    IndexIterator iter = a.getIndexIterator();
    double max = -Double.MAX_VALUE;
    double min = Double.MAX_VALUE;
    while (iter.hasNext()) {
      double val = iter.getDoubleNext();
      if (eval.isMissing(val))
        continue;
      if (val > max)
        max = val;
      if (val < min)
        min = val;
    }
    return new MAMath.MinMax(min, max);
  }

  public static double getMinimumSkipMissingData(Array a, double missingValue) {
    IndexIterator iter = a.getIndexIterator();
    double min = Double.MAX_VALUE;
    while (iter.hasNext()) {
      double val = iter.getDoubleNext();
      if ((val != missingValue) && (val < min))
        min = val;
    }
    return min;
  }

  public static double getMaximumSkipMissingData(Array a, double missingValue) {
    IndexIterator iter = a.getIndexIterator();
    double max = -Double.MAX_VALUE;
    while (iter.hasNext()) {
      double val = iter.getDoubleNext();
      if ((val != missingValue) && (val > max))
        max = val;
    }
    return max;
  }

  public static MAMath.MinMax getMinMaxSkipMissingData(Array a, double missingValue) {
    IndexIterator iter = a.getIndexIterator();
    double max = -Double.MAX_VALUE;
    double min = Double.MAX_VALUE;
    while (iter.hasNext()) {
      double val = iter.getDoubleNext();
      if (val == missingValue)
        continue;
      if (val > max)
        max = val;
      if (val < min)
        min = val;
    }
    return new MinMax(min, max);
  }

  /**
   * Set all the elements of this array to the given double value.
   * The value is converted to the element type of the array, if needed.
   *
   * @param result change this Array
   * @param val set all elements to this value
   */
  public static void setDouble(Array result, double val) {
    IndexIterator iter = result.getIndexIterator();
    while (iter.hasNext()) {
      iter.setDoubleNext(val);
    }
  }

  /**
   * sum all of the elements of array a as doubles.
   * The values from the array a are converted to double (if needed).
   * @param a read values from this Array
   * @return sum of elements
   */
  public static double sumDouble(Array a) {
    double sum = 0;
    IndexIterator iterA = a.getIndexIterator();
    while (iterA.hasNext()) {
      sum += iterA.getDoubleNext();
    }
    return sum;
  }

  /**
   * sum all of the elements of array a as doubles.
   * The values from the array a are converted to double (if needed).
   * @param a read values from this Array
   * @param missingValue skip values equal to this, or which are NaNs
   * @return sum of elements
   */
  public static double sumDoubleSkipMissingData(Array a, double missingValue) {
    double sum = 0;
    IndexIterator iterA = a.getIndexIterator();
    while (iterA.hasNext()) {
      double val = iterA.getDoubleNext();
      if ((val == missingValue) || Double.isNaN(val))
        continue;
      sum += val;
    }
    return sum;
  }

  /**
   * Holds a minimum and maximum value.
   */
  public static class MinMax {
    public double min, max;

    public MinMax(double min, double max) {
      this.min = min;
      this.max = max;
    }

    @Override
    public String toString() {
      return "MinMax{" +
              "min=" + min +
              ", max=" + max +
              '}';
    }
  }

  /**
   * Calculate the scale/offset for an array of numbers.
   * <pre>
   * If signed:
   *   then
   *     max value unpacked = 2^(n-1) - 1 packed
   *     min value unpacked = -(2^(n-1) - 1) packed
   *   note that -2^(n-1) is unused, and a good place to map missing values
   *   by solving 2 eq in 2 unknowns, we get:
   *     scale = (max - min) / (2^n - 2)
   *     offset = (max + min) / 2
   * If unsigned then
   *     max value unpacked = 2^n - 1 packed
   *     min value unpacked = 0 packed
   *   and:
   *     scale = (max - min) / (2^n - 1)
   *     offset = min
   *   One could modify this to allow a holder for missing values.
   * </pre>
   * @param a array to convert (not changed)
   * @param missingValue skip these
   * @param nbits map into this many bits
   * @return ScaleOffset, calculated as above.
   */
  public static MAMath.ScaleOffset calcScaleOffsetSkipMissingData(Array a, double missingValue, int nbits) {
    MAMath.MinMax minmax = getMinMaxSkipMissingData(a, missingValue);

    if (a.isUnsigned()) {
      long size = (1L << nbits) - 1;
      double offset = minmax.min;
      double scale =(minmax.max - minmax.min) / size;
      return new ScaleOffset(scale, offset);

    } else {
      long size = (1L << nbits) - 2;
      double offset = (minmax.max + minmax.min) / 2;
      double scale =(minmax.max - minmax.min) / size;
      return new ScaleOffset(scale, offset);
    }
  }

  public static Array convert2packed(Array unpacked, double missingValue, int nbits, DataType packedType) {
    MAMath.ScaleOffset scaleOffset = calcScaleOffsetSkipMissingData(unpacked, missingValue, nbits);
    Array result = Array.factory(packedType, unpacked.getShape());
    IndexIterator riter = result.getIndexIterator();
    while (unpacked.hasNext()) {
      double uv = unpacked.nextDouble();
      double pv = (uv - scaleOffset.offset) / scaleOffset.scale;
      riter.setDoubleNext( pv);
    }
    return result;
  }

  public static Array convert2Unpacked(Array packed, ScaleOffset scaleOffset) {
    Array result = Array.factory(DataType.DOUBLE, packed.getShape());
    IndexIterator riter = result.getIndexIterator();
    while (packed.hasNext())  {
      riter.setDoubleNext( packed.nextDouble() * scaleOffset.scale + scaleOffset.offset);
    }
    return result;
  }

  /**
   * Holds a scale and offset.
   */
  public static class ScaleOffset {
    public double scale, offset;

    public ScaleOffset(double scale, double offset) {
      this.scale = scale;
      this.offset = offset;
    }
  }

  /**
   * Returns true if the specified arrays have the same size, signedness, and <b>approximately</b> equal corresponding
   * elements. {@code float} elements must be within {@link Misc#defaultMaxRelativeDiffFloat} of each other, as
   * determined by {@link Misc#nearlyEquals(double, double, double)}. Similarly, {@code double} elements must be within
   * {@link Misc#defaultMaxRelativeDiffDouble} of each other.
   * <p>
   * {@link #equals(Array, Array)} is an alternative to this method that requires that corresponding elements be
   * <b>exactly</b> equal. It is suitable for use in {@link Object#equals} implementations, whereas this method isn't.
   *
   * @param data1  one array to be tested for equality.
   * @param data2  the other array to be tested for equality.
   * @return true if the specified arrays have the same size, signedness, and approximately equal corresponding elems.
   */
  public static boolean nearlyEquals(Array data1, Array data2) {
    if (data1 == data2) {  // Covers case when both are null.
      return true;
    } else if (data1 == null || data2 == null) {
      return false;
    }

    if (data1.getSize() != data2.getSize()) return false;
    if (data1.isUnsigned() != data2.isUnsigned()) return false;
    DataType dt = DataType.getType(data1);

    IndexIterator iter1 = data1.getIndexIterator();
    IndexIterator iter2 = data2.getIndexIterator();

    if (dt == DataType.DOUBLE) {
      while (iter1.hasNext() && iter2.hasNext()) {
        double v1 = iter1.getDoubleNext();
        double v2 = iter2.getDoubleNext();
        if (!Misc.nearlyEquals(v1, v2, Misc.defaultMaxRelativeDiffDouble))
          return false;
      }
    } else if (dt == DataType.FLOAT) {
      while (iter1.hasNext() && iter2.hasNext()) {
        float v1 = iter1.getFloatNext();
        float v2 = iter2.getFloatNext();
        if (!Misc.nearlyEquals(v1, v2, Misc.defaultMaxRelativeDiffFloat))
          return false;
      }
    } else if (dt.getPrimitiveClassType() == int.class) {
      while (iter1.hasNext() && iter2.hasNext()) {
        int v1 = iter1.getIntNext();
        int v2 = iter2.getIntNext();
        if (v1 != v2) return false;
      }
    } else if (dt.getPrimitiveClassType() == byte.class) {
      while (iter1.hasNext() && iter2.hasNext()) {
        short v1 = iter1.getShortNext();
        short v2 = iter2.getShortNext();
        if (v1 != v2) return false;
      }
    } else if (dt.getPrimitiveClassType() == short.class) {
      while (iter1.hasNext() && iter2.hasNext()) {
        byte v1 = iter1.getByteNext();
        byte v2 = iter2.getByteNext();
        if (v1 != v2) return false;
      }
    } else if (dt.getPrimitiveClassType() == long.class) {
      while (iter1.hasNext() && iter2.hasNext()) {
        long v1 = iter1.getLongNext();
        long v2 = iter2.getLongNext();
        if (v1 != v2) return false;
      }
    } else {
      while (iter1.hasNext() && iter2.hasNext()) {
        if (!Objects.equals(iter1.next(), iter2.next())) {
          return false;
        }
      }
    }

    return true;
  }

  /**
   * Returns true if the specified arrays have the same data type, shape, and equal corresponding elements. This method
   * is suitable for use in {@link Object#equals} implementations.
   * <p>
   * Note that floating-point elements must be exactly equal, not merely within some epsilon of each other. This is
   * because it's <b>impossible</b> to write a strictly-conforming {@link Object#equals} implementation when an
   * epsilon is incorporated, due to the transitivity requirement.
   * <p>
   * {@link #nearlyEquals} is an alternative to this method that returns true if the corresponding elements are
   * "approximately" equal to each other.
   *
   * @param array1 one array to be tested for equality.
   * @param array2 the other array to be tested for equality.
   * @return true if the specified arrays have the same data type, shape, and equal corresponding elements.
   * @see <a href=http://goo.gl/psfLb>Is there a way to get a hashcode of a float with epsilon? - Stack Overflow</a>
   */
  // TODO: Should we add this to Array as the Object.equals() implementation? How much work is that?
  public static boolean equals(Array array1, Array array2) {
    if (array1 == array2) {  // Covers case when both are null.
      return true;
    } else if (array1 == null || array2 == null) {
      return false;
    }

    // MAMath.nearlyEquals() does not require DataTypes to be equal, but in so doing, it becomes non-symmetric.
    // For example, suppose we have 2 arrays:
    //     ArrayLong  al;
    //     ArrayShort as;
    // If al contains elements that don't fit in a short, we could have the following:
    //     MAMath.nearlyEquals(al, as);  // true
    //     MAMath.nearlyEquals(as, al);  // false
    // This is because when MAMath.nearlyEquals() does comparisons, elements from the 2nd array are converted to the
    // type of the 1st array.
    //
    // In our implementation, we avoid this problem--and thus preserve symmetry--by insisting that the element
    // types of the 2 arrays are the same. This means that we lose some of the "flexibility" offered by the MAMath
    // version, but it turns out that it's not a good idea to compare different Number subtypes in the first place:
    // http://stackoverflow.com/questions/480632/why-doesnt-java-lang-number-implement-comparable
    if (array1.getDataType() != array2.getDataType()) {
      return false;
    }

    // MAMath.nearlyEquals() only requires that the 2 arrays have the same size, not the same shape. That was an
    // option I considered. Also, there's MAMath.conformable(), which returns true if shapes are equal after
    // reduction (e.g. { 3,4,5 } and { 3,1,4,1,5 } are conformable). By definition, conformable arrays have
    // the same size.
    //
    // Ultimately, I decided on the stricter requirement that the arrays must have the exact same shape. That's
    // because if 2 objects compare as equal, there's a general expectation that you can perform the same
    // operations on them and get the same result. But imagine this:
    //     Array a1 = Array.factory(DataType.INT, new int[] { 3,4 }, new int[] { 0,1,2,3,4,5,6,7,8,9,10,11 });
    //     Array a2 = Array.factory(DataType.INT, new int[] { 2,6 }, new int[] { 0,1,2,3,4,5,6,7,8,9,10,11 });
    // MAMath.nearlyEquals() will consider the arrays equal because they have the same size, but:
    //     a1.getInt(a1.getIndex().set(1, 1)) == 5
    //     a2.getInt(a2.getIndex().set(1, 1)) == 7
    if (!Arrays.equals(array1.getShape(), array2.getShape())) {
      return false;
    }

    // Note that we did not examine the Indexes of the arrays, nor their 1D backing stores. That's because we're
    // interested in the VIEWS of the data that the Arrays present, not with how the data is manipulated behind the
    // scenes.

    IndexIterator iter1 = array1.getIndexIterator();
    IndexIterator iter2 = array2.getIndexIterator();

    while (iter1.hasNext() && iter2.hasNext()) {
      if (!Objects.equals(iter1.next(), iter2.next())) {
        return false;
      }
      if (!Arrays.equals(iter1.getCurrentCounter(), iter2.getCurrentCounter())) {
        return false;
      }
    }

    assert !iter1.hasNext() && !iter2.hasNext();    // Iterators ought to be the same length.
    return true;
  }

  /**
   * An implementation of {@link Object#hashCode} that is consistent with {@link #equals(Array, Array)}.
   *
   * @param array an array to hash.
   * @return a hash code value for the array.
   */
  // TODO: Should we add this to Array as the Object.hashCode() implementation? How much work is that?
  public static int hashCode(Array array) {
    if (array == null) {
      return 0;
    }

    int hash = 3;
    hash = 29 * hash + array.getDataType().hashCode();
    hash = 29 * hash + Arrays.hashCode(array.getShape());

    // We can't simply hash array.getStorage(), because array may be a "view" that doesn't include all of the
    // elements in the backing store.
    for (IndexIterator iter = array.getIndexIterator(); iter.hasNext(); ) {
      hash = 29 * hash + iter.next().hashCode();
      hash = 29 * hash + Arrays.hashCode(iter.getCurrentCounter());
    }

    return hash;
  }
}


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

package ucar.nc2.ft2.coverage.adapter;

import ucar.nc2.dataset.CoordinateAxis1D;
import ucar.nc2.dataset.CoordinateAxis1DTime;
import ucar.nc2.dataset.CoordinateSystem;
import ucar.nc2.time.CalendarDate;
import ucar.nc2.time.CalendarDateRange;

import java.util.ArrayList;
import java.util.List;

/**
 * Grid Coordinate System Implementation
 * A Grid has all 1D coordinates.
 *
 * @author John
 * @since 12/25/12
 */
public class GridCS extends DtCoverageCS {

  GridCS(DtCoverageCSBuilder builder) {
    super(builder);
  }

  @Override
  public boolean isRegularSpatial() {
    return getXHorizAxis().isRegular() && getYHorizAxis().isRegular();
  }

  @Override
  public CoordinateAxis1D getXHorizAxis() {
    return (CoordinateAxis1D) super.getXHorizAxis();
  }

  @Override
  public CoordinateAxis1D getYHorizAxis() {
    return (CoordinateAxis1D) super.getYHorizAxis();
  }

  // LOOK another possibility is a scalar runtime and a 1D time offset

  @Override
  public CoordinateAxis1DTime getTimeAxis() {
    return (CoordinateAxis1DTime) super.getTimeAxis();
  }

  /*
   @Override
 public List<CalendarDate> getCalendarDates() {
    if (getTimeAxis() != null)
      return getTimeAxis().getCalendarDates();

    else if (getRunTimeAxis() != null)
      return getRunTimeAxis().getCalendarDates();

    else
      return new ArrayList<>();
  }
  */

  @Override
  public CalendarDateRange getCalendarDateRange() {
    if (getTimeAxis() != null)
      return getTimeAxis().getCalendarDateRange();

    else if (getRunTimeAxis() != null)
      return getRunTimeAxis().getCalendarDateRange();

    else
      return null;
  }

}


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

import thredds.client.catalog.Access;
import thredds.client.catalog.Catalog;
import thredds.client.catalog.CatalogRef;
import thredds.client.catalog.Dataset;
import thredds.client.catalog.builder.AccessBuilder;
import thredds.client.catalog.builder.CatalogBuilder;
import thredds.client.catalog.builder.CatalogRefBuilder;
import thredds.client.catalog.builder.DatasetBuilder;
import thredds.server.catalog.builder.CatalogScanBuilder;
import thredds.server.catalog.builder.FeatureCollectionRefBuilder;

import javax.annotation.concurrent.Immutable;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * TDS Configuration Catalog
 *
 * @author caron
 * @since 1/15/2015
 */
@Immutable
public class ConfigCatalog extends Catalog {

  public ConfigCatalog(URI baseURI, String name, Map<String, Object> flds, List<DatasetBuilder> datasets) {
    super(baseURI, name, flds, datasets);
  }

  public List<DatasetRootConfig> getDatasetRoots() {
    return (List<DatasetRootConfig>) getLocalFieldAsList(Catalog.DatasetRoots);
  }

  public List<CatalogScan> getCatalogScans() {
    List<CatalogScan> result = new ArrayList<>();
    for (Dataset ds : getDatasetsLocal())
      if (ds instanceof CatalogScan)
        result.add((CatalogScan) ds);
    return result;
  }

  // turn ConfigCatalog into a mutable CatalogBuilder so we can mutate
  public CatalogBuilder makeCatalogBuilder() {
    CatalogBuilder builder = new CatalogBuilder(this);
    for (Dataset ds : getDatasetsLocal()) {
      builder.addDataset(makeDatasetBuilder(null, ds));
    }
    return builder;
  }

  private DatasetBuilder makeDatasetBuilder(DatasetBuilder parent, Dataset ds) {

    DatasetBuilder builder;
    if (ds instanceof CatalogScan)
      builder = new CatalogScanBuilder(parent, (CatalogScan) ds);
    else if (ds instanceof FeatureCollectionRef)
      builder = new FeatureCollectionRefBuilder(parent, (FeatureCollectionRef) ds);
    else if (ds instanceof CatalogRef)
      builder = new CatalogRefBuilder(parent, (CatalogRef) ds);
    else
      builder = new DatasetBuilder(parent, ds);

    List<Access> accesses = (List<Access>) ds.getLocalFieldAsList(Dataset.Access);
    for (Access access : accesses)
      builder.addAccess(new AccessBuilder(builder, access));

    if (!(ds instanceof CatalogRef)) {
      for (Dataset nested : ds.getDatasetsLocal())
        builder.addDataset(makeDatasetBuilder(builder, nested));
    }

    return builder;
  }

    /* static public ConfigCatalog makeCatalogWithServices(ConfigCatalog cc, List<Service> services) {
    Map<String, Object> flds = new HashMap<>();

    for (Map.Entry<String, Object> entry : cc.getFldIterator()) {
      flds.put(entry.getKey(), entry.getValue());
    }
    flds.put(Catalog.Services, services);

    //   public ConfigCatalog(URI baseURI, String name, Map<String, Object> flds, List<DatasetBuilder> datasets) {
    return new ConfigCatalog(cc.getBaseURI(), cc.getName(), flds, null);
  }  */

}


/////////////////////////////////////////////////////////////////////////////
// This file is part of the "Java-DAP" project, a Java implementation
// of the OPeNDAP Data Access Protocol.
//
// Copyright (c) 2010, OPeNDAP, Inc.
// Copyright (c) 2002,2003 OPeNDAP, Inc.
//
// Author: James Gallagher <jgallagher@opendap.org>
//
// All rights reserved.
//
// Redistribution and use in source and binary forms,
// with or without modification, are permitted provided
// that the following conditions are met:
//
// - Redistributions of source code must retain the above copyright
//   notice, this list of conditions and the following disclaimer.
//
// - Redistributions in binary form must reproduce the above copyright
//   notice, this list of conditions and the following disclaimer in the
//   documentation and/or other materials provided with the distribution.
//
// - Neither the name of the OPeNDAP nor the names of its contributors may
//   be used to endorse or promote products derived from this software
//   without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
// TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
/////////////////////////////////////////////////////////////////////////////

package opendap.dap;

import java.util.Enumeration;
import java.util.Vector;

import opendap.dap.parsers.*;
import opendap.util.Debug;

import java.io.*;

/**
 * The Data Attribute Structure is a set of name-value pairs used to
 * describe the data in a particular dataset.  The name-value pairs
 * are called the "attributes."  The values may be of any of the
 * OPeNDAP simple data types (DByte, DInt32, DUInt32, DFloat64, DString and
 * DURL), and may be scalar or vector.  (Note that all values are
 * actually stored as string data.)
 *

 * A value may also consist of a set of other name-value pairs.  This
 * makes it possible to nest collections of attributes, giving rise
 * to a hierarchy of attributes.  OPeNDAP uses this structure to provide
 * information about variables in a dataset.
 *

 * In the following example of a DAS, several of the attribute
 * collections have names corresponding to the names of variables in
 * a hypothetical dataset.  The attributes in that collection are said to
 * belong to that variable.  For example, the lat variable has an
 * attribute units of degrees_north.
 *

 *

 *  Attributes {
 *      GLOBAL {
 *          String title "Reynolds Optimum Interpolation (OI) SST";
 *      }
 *      lat {
 *          String units "degrees_north";
 *          String long_name "Latitude";
 *          Float64 actual_range 89.5, -89.5;
 *      }
 *      lon {
 *          String units "degrees_east";
 *          String long_name "Longitude";
 *          Float64 actual_range 0.5, 359.5;
 *      }
 *      time {
 *          String units "days since 1-1-1 00:00:00";
 *          String long_name "Time";
 *          Float64 actual_range 726468., 729289.;
 *          String delta_t "0000-00-07 00:00:00";
 *      }
 *      sst {
 *          String long_name "Weekly Means of Sea Surface Temperature";
 *          Float64 actual_range -1.8, 35.09;
 *          String units "degC";
 *          Float64 add_offset 0.;
 *          Float64 scale_factor 0.0099999998;
 *          Int32 missing_value 32767;
 *      }
 *  }
 *

 *

 * Attributes may have arbitrary names, although in most datasets it
 * is important to choose these names so a reader will know what they
 * describe.  In the above example, the GLOBAL attribute provides
 * information about the entire dataset.
 *

 * Data attribute information is an important part of the the data
 * provided to a OPeNDAP client by a server, and the DAS is how this
 * data is packaged for sending (and how it is received).
 *
 * @author jehamby
 * @version $Revision: 22644 $
 * @see DDS
 * @see AttributeTable
 * @see Attribute
 */
public class DAS extends AttributeTable
{

    // Used by resolveAliases() method
    private AttributeTable currentAT = null;
    private Alias currentAlias = null;

    private BaseTypeFactory factory = null;
    /**
     * Create a new empty DAS.
     */
    public DAS()
    {
        super("Attributes");
        factory = new DefaultFactory();
    }

   public boolean parse(InputStream stream) throws ParseException, DAP2Exception
   {
        try {
            String text = DConnect2.captureStream(stream);
            //System.err.println("---------"+text+"\n----------------"); System.err.flush();
            return parse(text);
        } catch (IOException ioe) {
            throw new ParseException("Cannot read DAS",ioe);
        }
   }

    public boolean parse(String text) throws ParseException,DAP2Exception
    {
        Dap2Parser parser = new Dap2Parser(factory);
        int result = parser.dasparse(text,this);
        if(result == Dap2Parse.DapERR)
            throw parser.getERR();
        return (result == Dap2Parse.DapDAS ? true : false);
    }

    /**
     * Returns the AttributeTable with the given name.
     *
     * @param name the name of the AttributeTable to return.
     * @return the AttributeTable with the specified name, or null
     *         if there is no matching AttributeTable.
     * @throws NoSuchAttributeException There is no AttributeTable with the passed name.
     * @see AttributeTable
     */
    public final AttributeTable getAttributeTable(String name) throws NoSuchAttributeException
    {
        AttributeTable at = null;
        Attribute a = getAttribute(name);
        if (a != null) {
            if (a.isContainer()) {
                at = a.getContainer();
            }
        }
        return (at);
    }

    /**
     * Returns the AttributeTable with the given name.
     *
     * @param name the name of the AttributeTable to return.
     * @return the AttributeTable with the specified name, or null
     *         if there is no matching AttributeTable.
     * @see AttributeTable
     */
    public final AttributeTable getAttributeTableN(String name)
    {
        AttributeTable at = null;
        Attribute a = getAttribute(name);
        if (a != null) {
            if (a.isContainer()) {
                at = a.getContainerN();
            }
        }
        return (at);
    }

    /**
     * Adds an AttributeTable to the DAS.
     *
     * @param name the name of the AttributeTable to add.
     * @param a    the AttributeTable to add.
     * @see AttributeTable
     */
    public void addAttributeTable(String name, AttributeTable a) throws AttributeExistsException
    {
        addContainer(name, a);
    }

    /**
     * This method searchs through the DAS
     * for Alias members. When an Alias is found the method attempts to
     * resolve it to a specific Attribute.
     *

     * This method is invoked by parse(InputStream is), and is
     * used to search for Aliases in AttributeTables found in the DAS.
     *

     * If you are building a DAS from it's API it is important to call
     * this method prior to returning said DAS to an application. If
     * this call is not made, Aliases will not work correctly.
     *
     * @see Alias
     * @see DDS#resolveAliases()
     */
    public void resolveAliases() throws MalformedAliasException, UnresolvedAliasException, NoSuchAttributeException
    {

        resolveAliases(this);

        // Enforce the rule that Aliases at the highest level of the DAS
        // must point to a container (AttributeTable)
        Enumeration e = getNames();
        while (e.hasMoreElements()) {
            String aName = (String) e.nextElement();

            if (Debug.isSet("DAS")) {
		DAPNode.log.debug("DAS.resolveAliases() - aName: " + aName);
	    }
            Attribute at = getAttribute(aName);
            if (at == null || !at.isContainer()) {

                throw new MalformedAliasException("Aliases at the top-level of a DAS MUST reference a container (AttributeTable), not a simple Attribute");
            }
        }

    }

    /**
     * This method recursively searchs through the passed AttributeTable parameter
     * at for Alias members. When an Alias is found the method attempts to
     * resolve it to a specific Attribute.
     *

     * This method gets called is invoked by reolveAliases(BaseType bt), and is
     * used to search for Aliases in AttributeTables found in a BaseTypes Attributes.
     *

     * This method manipulates the global variable currentBT.
     *
     * @param at The AttributeTable in which to search for and resolve Alias members
     */

    private void resolveAliases(AttributeTable at) throws MalformedAliasException, UnresolvedAliasException, NoSuchAttributeException
    {
        // Cache the current (parent) Attribute table. This value is
        // null if this method is called from parse();
        AttributeTable cacheAT = currentAT;
        try {

        // Set the current AttributeTable to the one that we are searching.
        currentAT = at;

        if (Debug.isSet("DAS")) {
		    DAPNode.log.debug("DAS.resolveAliases(at=" + at + ")");
	    }

        //getall of the Attributes from the table.
        Enumeration aNames = at.getNames();
        while (aNames.hasMoreElements()) {

            String aName = (String) aNames.nextElement();
            if (Debug.isSet("DAS")) {
		DAPNode.log.debug("DAS.resolveAliases(at=" + at + ") - aName: " + aName);
	    }

            opendap.dap.Attribute thisA = currentAT.getAttribute(aName);

            if (Debug.isSet("DAS")) {
		DAPNode.log.debug("thisA.getClass().getName(): " + thisA.getClass().getName());
	    }

            if (thisA.isAlias()) {
                //Is Alias? Resolve it!
                resolveAlias((Alias) thisA);
                if (Debug.isSet("DAS")) {
		DAPNode.log.debug("Resolved Alias: '" + thisA.getEncodedName() + "'\n");
	    }
            } else if (thisA.isContainer()) {
                //Is AttributeTable (container)? Search it!

                resolveAliases(thisA.getContainer());

            }
        }
    } finally {
        // Restore the previous currentAT state.
        currentAT = cacheAT;
}

    }

    /**
     * This method attempts to resolve the past Alias to a specific Attribute in the DDS.
     * It does this by:
     *

     *
1) Tokenizing the Alias's variable and attribute fields (see Alias)

     *
2) Evaluating the tokenized fields to determine if the Alias is defined in terms
     * of a relative or absolute path

     *
3) Searching the DAS or the currentAT (depending on results
     * of 2) for the target Attribute

     *
4) Setting the Aliases intrnal reference to it's Attribute
     *

     *

     * If an Attribute matching the definition of the Alias cannot be located,
     * an Exception is thrown
     *
     * @param alias The Alias which needs to be resolved
     */

    private void resolveAlias(Alias alias) throws MalformedAliasException, UnresolvedAliasException
    {

        //Get the crucial stuff out of the Alias
        String name = alias.getClearName();
        String attribute = alias.getAliasedToAttributeFieldAsClearString();

        // Get ready!
        Enumeration e = null;
        currentAlias = alias;

        if (Debug.isSet("DAS")) {
		DAPNode.log.debug("\n\nFound: Alias " + name +
                    "  " + attribute);
	    }

        // Let's go
        // see if we can find an Attribute within that DAS that matches the attribute field
        // in the Alias declartion.

        // The Attribute field MAY NOT be empty.
        if (attribute.equals("")) {
            throw new MalformedAliasException("The attribute 'attribute' in the Alias " +
                    "element must have a value other than an empty string.");
        }

        if (Debug.isSet("DAS")) {
		DAPNode.log.debug("Attribute: `" + attribute + "'");
	    }

        // Tokenize the attribute field.

        Vector aNames = opendap.dap.DDS.tokenizeAliasField(attribute);

        if (Debug.isSet("DAS")) {
	    DAPNode.log.debug("Attribute name tokenized to " + aNames.size() + " elements");
            e = aNames.elements();
            while (e.hasMoreElements()) {
                String aname = (String) e.nextElement();
                DAPNode.log.debug("name: " + aname);
            }
        }

        opendap.dap.Attribute targetAT = null;

        // Absolute paths for attributes names must start with the dot character.
        boolean isAbsolutePath = aNames.get(0).equals(".");

        if (isAbsolutePath) { //Is it an absolute path?

            if (aNames.size() == 1) {
                throw new MalformedAliasException("Aliases must reference an Attribute. " +
                        "An attribute field of dot (.) references the entire " +
                        "DAS, which is not allowed.");
            } else {
                // Dump the dot from the vector of tokens and go try to find
                // the Attribute in the DAS.
                aNames.remove(0);

                targetAT = getAliasAttribute(this, aNames);
            }
        } else {
            throw new MalformedAliasException("In the Alias '" + name + "'" +
                    " the attribute 'attribute' does not begin with the character dot (.). " +
                    "The 'attribute' field must always be an absoulute path name from the " +
                    "top level of the dataset, and thus must always begin with the dot (.) character.");
        }

        alias.setMyAttribute(targetAT);

    }

    /**
     * This method executes a (recursive) search of the AttributeTable
     * parameter at for an Attribute whose name resolves to
     * the vector of names contained in the Vector parameter
     * aNames. An Attribute is considered a match if each of it's node
     * names in the hierarchy of AttributeTables contained in the
     * one passed as parameter at matches (equals) the corresponding name
     * in the Vector aNames.
     *
     * @param att    The AttributeTable to search
     * @param aNames The Vector of names to match to the nodes of at
     */

    private opendap.dap.Attribute getAliasAttribute(AttributeTable att, Vector aNames)
            throws MalformedAliasException, UnresolvedAliasException
    {

        // Get the first node name form the vector.
        String aName = (String) aNames.get(0);

        // Get the list of child nodes from the AttributeTable
        Enumeration e = att.getNames();
        while (e.hasMoreElements()) {

            // Get an Attribute
            String atName = (String) e.nextElement();
            opendap.dap.Attribute a = att.getAttribute(atName);

            // Get the Attributes name and Normalize it.
            String normName = opendap.dap.DDS.normalize(a.getEncodedName());

            // Are they the same?
            if (normName.equals(aName)) {

                // Make sure this reference doesn't pass through an Alias.
                if (a.isAlias()) {
                    throw new MalformedAliasException("Aliases may NOT point to other aliases");
                }

                //dump the name from the list of names.
                aNames.remove(0);

                // Are there more?
                if (aNames.size() == 0) {
                    //No! We found it!
                    return (a);

                } else if (a.isContainer()) { // Is this Attribute a container (it better be)

                    try {
                        // Recursively search for the rest of the name vector in the container.
                        return (getAliasAttribute(a.getContainer(), aNames));

                    } catch (NoSuchAttributeException nsae) {
                        throw new MalformedAliasException("Attribute " + a.getEncodedName() +
                                " is not an attribute container. (AttributeTable) " +
                                " It may not contain the attribute: " +
                                aName);
                    }

                } else { // Dead-end, through an exception!

                    throw new MalformedAliasException("Attribute " + a.getEncodedName() +
                            " is not an attribute container. (AttributeTable) " +
                            " It may not contain the attribute: " +
                            aName);
                }

            }
        }
        // Nothing Matched, so this search failed.
        throw new UnresolvedAliasException("The alias `" + currentAlias.getEncodedName() +
                "` references the attribute: `" + aName + "` which cannot be found.");

    }

    /**
     * Returns a clone of this Attribute.
     * See DAPNode.cloneDag()
     *
     * @param map track previously cloned nodes
     * @return a clone of this Attribute.
     */
    public DAPNode cloneDAG(CloneMap map)
        throws CloneNotSupportedException
    {
        DAS das = (DAS) super.cloneDAG(map);
        return das;
    }

}