package nxm.sys.libm;

/** Compares two numbers for near equality.
    Default is a relative tolerance which is good to compare how much two number
    differ and is good for most numbers, but is not good when the two numbers
    are close or equal to zero.
    <br><pre>  Relative tolerance comparison:
              abs(d1 - d2)
       ------------------------   &lt;  tolerance
       (abs(d1) + abs(d2)) / 2
    </pre>
    <br><pre>  Absolute tolerance (delta range) comparison:
      abs(d1 - d2) &lt;= tolerance
    </pre>
   @author  Ken Smith / Jay Hallam
   @author  Neon Ngo (since 3.1.0/3.3.1: added equals with absolute tolerance)
*/
public class Tolerance {

  /** This constructor was automatically provided by Java,
   *  but it is unneeded as all the methods are static methods.
   *  @deprecated now that Javadocs are required, it is temporarily
   *              being explicitely provided but deprecated
   */
  @Deprecated
  public Tolerance () {}

  /** default system-wide tolerance for floating point numbers comparisons. */
  private static double defaultTolerance = 1.e-6;

  private static double SMALLEST_ALLOWED = 1e-35;

  /** Set the system-wide tolerance for floating point numbers comparisons.
      This implies that all future callers use the new tolerance system-wide.
      Since 3.1.0: the absolute value of tolr is used (otherwise it would result in
      most equalsWithTolerance/EQT comparisons to fail when they do not exactly equal.)
      @param tolr new system-wide floating point tolerance
  */
  public static void setTolerance (double tolr) {
    defaultTolerance = Math.abs(tolr);
  }

  /** Get the current system-wide floating point tolerance setting.
   *  @return the current system-wide floating point tolerance setting */
  public static double getTolerance() {
    return defaultTolerance;
  }

  /** compares two floating point numbers and returns TRUE if the two inputs are
      equal within the system-wide floating point tolerance (relatively).
      @param d1 value 1
      @param d2 value 2
      @return TRUE if the two inputs are within tolerance,FALSE otherwise
  */
  public static boolean equalsWithTolerance (double d1, double d2) {
    return equalsWithTolerance (d1,d2,defaultTolerance);
  }

  /** compares two floating point numbers and returns TRUE if the two inputs are
      equal within the specified floating point tolerance (relatively).
      @param d1 value 1
      @param d2 value 2
      @param tolr double tolerance (if negative, then use configured system-wide
                  tolerance value from {@link #getTolerance()}
      @return TRUE if the two inputs are within tolerance, FALSE otherwise
  */
  public static boolean equalsWithTolerance (double d1, double d2, double tolr) {
    if (tolr==0) return (d1 == d2);
    if (tolr<0) tolr = defaultTolerance;  // use default system-wide tolerance
    return eqt(d1,d2,tolr);
  }

  /** Equals with relative tolerance, but without extra checks on tolr to speed up array usage. */
  private static boolean eqt (double d1, double d2, double tolr) {
    if (d1 == d2) return true;
    if (Math.abs(d1) < SMALLEST_ALLOWED && Math.abs(d2) < SMALLEST_ALLOWED) return true; // Avoid divide by zero
    double error = Math.abs (d1 - d2) / ((Math.abs(d1) + Math.abs(d2))/2.0);
    return (error < tolr);
  }

  /** Tests to see of the elements in two arrays are the same.
      @param array1 The 1st array (can be a double[], float[], long[], int[], short[], byte[], or java.lang.Number[]).
      @param off1   The offset into the 1st array to start checking at.
      @param array2 The 2nd array (must have same type as array1).
      @param off2   The offset into the 2nd array to start checking at.
      @param length The number of elements to check.
      @param tolr   The tolerance (-1 = use system-wide default tolerance, 0 = no tolerance, must be exact).
      @return true if all of the elements checked are the same (within the given relative tolerance)
              false if any of them differ.
      @throws ClassCastException if array1 and array2 are not the same type.
      @throws ArrayIndexOutOfBoundsException If the off1, off2 and size values cause either of
              the arrays to be accessed out of bounds (this error is not assured since it is
              checked as the arrays are accessed, if the values differ significantly, it may
              break out of the checks early and return false).
      @since NeXtMidas 2.7.0
   */
  public static boolean equalsWithTolerance (Object array1, int off1, Object array2, int off2, int length, double tolr) {
    // NOTE: This method is mostly a duplicated by equalsWithAbsTol(...).
    //       So changes here should probably be replicated there.
    if (tolr<0) tolr = defaultTolerance;  // use default system-wide tolerance
    int i  = off1;
    int j  = off2;
    int ii = i + length;

    // Note: tolr < 1 on integer values is the same as an equals check (for performance)
    if (array1 instanceof double[]) {
      double[] a1 = (double[])array1;
      double[] a2 = (double[])array2;
      if (tolr == 0) for (;i<ii;i++,j++) { if (a1[i] != a2[j])         return false; } // <- for performance
      else           for (;i<ii;i++,j++) { if (!eqt(a1[i],a2[j],tolr)) return false; }
    }
    else if (array1 instanceof float[]) {
      float[] a1 = (float[])array1;
      float[] a2 = (float[])array2;
      if (tolr == 0) for (;i<ii;i++,j++) { if (a1[i] != a2[j])         return false; } // <- for performance
      else           for (;i<ii;i++,j++) { if (!eqt(a1[i],a2[j],tolr)) return false; }
    }
    else if (array1 instanceof long[]) {
      long[] a1 = (long[])array1;
      long[] a2 = (long[])array2;
      if (tolr <  1) for (;i<ii;i++,j++) { if (a1[i] != a2[j])         return false; } // <- for performance (integer comparison)
      else           for (;i<ii;i++,j++) { if (!eqt(a1[i],a2[j],tolr)) return false; }
    }
    else if (array1 instanceof int[]) {
      int[] a1 = (int[])array1;
      int[] a2 = (int[])array2;
      if (tolr <  1) for (;i<ii;i++,j++) { if (a1[i] != a2[j])         return false; } // <- for performance (integer comparison)
      else           for (;i<ii;i++,j++) { if (!eqt(a1[i],a2[j],tolr)) return false; }
    }
    else if (array1 instanceof short[]) {
      short[] a1 = (short[])array1;
      short[] a2 = (short[])array2;
      if (tolr <  1) for (;i<ii;i++,j++) { if (a1[i] != a2[j])         return false; } // <- for performance (integer comparison)
      else           for (;i<ii;i++,j++) { if (!eqt(a1[i],a2[j],tolr)) return false; }
    }
    else if (array1 instanceof byte[]) {
      byte[] a1 = (byte[])array1;
      byte[] a2 = (byte[])array2;
      if (tolr <  1) for (;i<ii;i++,j++) { if (a1[i] != a2[j])         return false; } // <- for performance (integer comparison)
      else           for (;i<ii;i++,j++) { if (!eqt(a1[i],a2[j],tolr)) return false; }
    }
    else if (array1 instanceof Number[]) { // since 3.3.1: to support Double[], Float[], Long[], etc.
      Number[] a1 = (Number[])array1;
      Number[] a2 = (Number[])array2;
      if (tolr == 0) for (;i<ii;i++,j++) { if (a1[i].equals(a2[j]))     return false; } // <- for performance
      else           for (;i<ii;i++,j++) { if (!eqt(a1[i].doubleValue(),a2[j].doubleValue(),tolr)) return false; }
    }
    else {
      throw new IllegalArgumentException("Illegal type "+array1.getClass());
    }
    return true; // all element are the same
  }

  /** compares two floating point numbers and returns TRUE if the two inputs are
      equal within the system-wide floating point absolute tolerance (i.e. delta range).
      <br>I.e. the result of: <code> abs(d1 - d2) &lt;= {@link #getTolerance()} </code>
      @param d1 value 1
      @param d2 value 2
      @return true if the two inputs are within an absolute tolerance (i.e. delta range), false otherwise
      @since NeXtMidas 3.1.0
   */
  public static boolean equalsWithAbsTol (double d1, double d2) {
    if (d1 == d2) return true;                    // quick check for true equality
    return Math.abs(d1 - d2) <= defaultTolerance; // absolute tolerance check
  }

  /** compares two floating point numbers and returns TRUE if the two inputs are
      equal within the specified floating point absolute tolerance (delta range).
      <br>I.e. the result of:<code> abs(d1 - d2) &lt;= tol </code>
      @param d1 value 1
      @param d2 value 2
      @param delta absolute (delta range) tolerance (if negative, then use configured system-wide
                   tolerance value from {@link #getTolerance()}
      @return true if the two inputs are within an absolute tolerance (i.e. delta range), false otherwise
      @since NeXtMidas 3.1.0
   */
  public static boolean equalsWithAbsTol (double d1, double d2, double delta) {
    if (d1 == d2) return true;               // quick check for true equality
    if (delta < 0) delta = defaultTolerance; // use default system-wide tolerance
    return Math.abs(d1 - d2) <= delta;       // absolute tolerance check
  }

  /**
   * Compare two doubles and returns TRUE if the two inputs are effectively equal
   * within the precision of Java doubles (accomplished by using Math.ulp)
   * .
   * Note: Math.ulp(double) gives the positive distance between this double
   * and the double value next larger in magnitude
   *
   * @param d1 value 1
   * @param d2 value 2
   * @return true if the two inputs are equal within within the precision of Java doubles
   * @since NeXtMidas 3.7.1
   */
  public static boolean equals(double d1, double d2) {
    if (d1 == d2) return true;
    double d1_ulp = Math.ulp(d1);
    return (d2 >= d1 - d1_ulp) && (d2 <= d1 + d1_ulp);
  }

  /**
   * Compare two floats and returns TRUE if the two inputs are effectively equal
   * within the precision of Java floats (accomplished by using Math.ulp)
   * .
   * Note: Math.ulp(float) gives the positive distance between this float
   * value and float value next larger in magnitude
   *
   * @param f1 value 1
   * @param f2 value 2
   * @return true if the two inputs are equal within within the precision of Java floats
   * @since NeXtMidas 3.7.1
   */
  public static boolean equals(float f1, float f2) {
    if (f1 == f2) return true;
    double f1_ulp = Math.ulp(f1);
    return (f2 >= f1 - f1_ulp) && (f2 <= f1 + f1_ulp);
  }

  /** Tests to see of the elements in two arrays are the same within absolute tolerance (delta range).
      @param array1 The 1st array (can be a double[], float[], long[], int[], short[], byte[], or java.lang.Number[]).
      @param off1   The offset into the 1st array to start checking at.
      @param array2 The 2nd array (must have same type as array1).
      @param off2   The offset into the 2nd array to start checking at.
      @param length The number of elements to check.
      @param delta   The absolute (delta range) tolerance (-1 = use system-wide default tolerance, 0 = no tolerance, must be exact).
      @return true if all of the elements checked are the same (within the given absolute tolerance / delta range)
                false if any of them differ.
      @throws ClassCastException if array1 and array2 are not the same type.
      @throws ArrayIndexOutOfBoundsException If the off1, off2 and size values cause either of
              the arrays to be accessed out of bounds (this error is not assured since it is
              checked as the arrays are accessed, if the values differ significantly, it may
              break out of the checks early and return false).
     @since NeXtMidas 3.3.1
   */
  public static boolean equalsWithAbsTolerance (Object array1, int off1, Object array2, int off2, int length, double delta) {
    // NOTE: This method is mostly a duplicate of equalsWithTolerance(...), except that it calls equalsWithAbsTol(..).
    //       So changes here should probably be replicated there.
    if (delta<0) delta = defaultTolerance;  // use default system-wide tolerance
    int i  = off1;
    int j  = off2;
    int ii = i + length;

    // Note: tolr < 1 on integer values is the same as an equals check (for performance)
    if (array1 instanceof double[]) {
      double[] a1 = (double[])array1;
      double[] a2 = (double[])array2;
      if (delta == 0) for (;i<ii;i++,j++) { if (a1[i] != a2[j])                       return false; } // <- for performance
      else            for (;i<ii;i++,j++) { if (!equalsWithAbsTol(a1[i],a2[j],delta)) return false; }

    }
    else if (array1 instanceof float[]) {
      float[] a1 = (float[])array1;
      float[] a2 = (float[])array2;
      if (delta == 0) for (;i<ii;i++,j++) { if (a1[i] != a2[j])                       return false; } // <- for performance
      else            for (;i<ii;i++,j++) { if (!equalsWithAbsTol(a1[i],a2[j],delta)) return false; }
    }
    else if (array1 instanceof long[]) {
      long[] a1 = (long[])array1;
      long[] a2 = (long[])array2;
      if (delta <  1) for (;i<ii;i++,j++) { if (a1[i] != a2[j])                       return false; } // <- for performance (integer comparison)
      else            for (;i<ii;i++,j++) { if (!equalsWithAbsTol(a1[i],a2[j],delta)) return false; }
    }
    else if (array1 instanceof int[]) {
      int[] a1 = (int[])array1;
      int[] a2 = (int[])array2;
      if (delta <  1) for (;i<ii;i++,j++) { if (a1[i] != a2[j])                       return false; } // <- for performance (integer comparison)
      else            for (;i<ii;i++,j++) { if (!equalsWithAbsTol(a1[i],a2[j],delta)) return false; }
    }
    else if (array1 instanceof short[]) {
      short[] a1 = (short[])array1;
      short[] a2 = (short[])array2;
      if (delta <  1) for (;i<ii;i++,j++) { if (a1[i] != a2[j])                       return false; } // <- for performance (integer comparison)
      else            for (;i<ii;i++,j++) { if (!equalsWithAbsTol(a1[i],a2[j],delta)) return false; }
    }
    else if (array1 instanceof byte[]) {
      byte[] a1 = (byte[])array1;
      byte[] a2 = (byte[])array2;
      if (delta <  1) for (;i<ii;i++,j++) { if (a1[i] != a2[j])                       return false; } // <- for performance (integer comparison)
      else            for (;i<ii;i++,j++) { if (!equalsWithAbsTol(a1[i],a2[j],delta)) return false; }
    }
    else if (array1 instanceof Number[]) {
      Number[] a1 = (Number[])array1;
      Number[] a2 = (Number[])array2;
      if (delta == 0) for (;i<ii;i++,j++) { if (a1[i].equals(a2[j]))                  return false; } // <- for performance
      else            for (;i<ii;i++,j++) { if (!eqt(a1[i].doubleValue(),a2[j].doubleValue(),delta)) return false; }
    }
    else {
      throw new IllegalArgumentException("Illegal type "+array1.getClass());
    }
    return true; // all element are the same
  }
}
