package nxm.sys.test.junit;

import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;

import nxm.sys.inc.ProvisionalUseOnly;
import nxm.sys.lib.Data;
import nxm.sys.libm.Tolerance;

/**
 * Hamcrest Matcher for unit testing of nxm.sys.lib.Data values with more informative failure descriptions.
 *
 * @author Neon Ngo
 * @version $Id: IsDataEqual.java,v 1.3 2015/12/01 16:41:51 ntn Exp $
 * @since NeXtMidas 3.7.0
 */
@ProvisionalUseOnly("Added in NeXtMidas 3.7.0")
public class IsDataEqual<T> extends BaseMatcher<T> {
  private final Data   expected;
  /** delta is the absolute tolerance (null for none and hence no type promotion) */
  private final Double delta;

  /**
   * Matcher that matches when the examined object is logically equal to the specified {@code expectedVal} as determined
   * by calling the {@link Data#equals(Data)} method on the <b>examined</b> object without any type promotion/casting.
   * @param expectedData expected value
   * @see #IsDataEqual(T, Double)
   */
  public IsDataEqual (T expectedVal) {
    this(expectedVal, null);
  }

  /**
   * Matcher that matches when the examined Data object is logically equal to the specified {@code expectedVal}. <br>
   * 1. If {@code absoluteTolerance} is not null, equality is determined by calling:
   *    {@link Data#equals(Data, int, double)} method on the <b>examined</b> object with type promotion (e.g.
   *    type 'F', 'I', etc. will be promoted to higher precision type via {@link Data#promoteType(byte, byte)}.
   *    NOTE: as of 3.7.0, we don't have a flag to NOT promote format type along with doing tolerance equality test
   *          using the Data.equals(Data data, int flags, double tolerance) method.
   * <br>
   * 2. Otherwise equality is determined by calling:
   *    {@link Data#equals(Data)} method on the <b>examined</b> object without any type promotion/casting.
   * <br>
   * NOTE: to do type promotion comparison without tolerance, set it to zero.
   * @param expectedVal expected value (Data or any Object that the {@link Data#Data(Object)} constructor supports.)
   * @param delta absolute tolerance when comparing actual to expected value<br>
   *              (null = no tolerance and no type promotion;<br>
   *                0 = no tolerance (must be exact) with type promotion;<br>
   *               -1 = use system-wide default tolerance in {@link Tolerance#getTolerance()}) with type promotion.
   * @throws NullPointerException if expectedVal is null
   * @throws IllegalArgumentException for invalid absoluteTolerance values (NaN, Infinity)
   */
  public IsDataEqual (T expectedVal, Double delta) {
    super();
    if (expectedVal == null) throw new NullPointerException("expected value cannot be null!");

    if (expectedVal instanceof Data) this.expected = (Data) expectedVal;
    else                             this.expected = new Data(expectedVal);

    if (delta != null) {
      if (Double.isNaN(delta))      throw new IllegalArgumentException("absolute tolerance (delta) cannot be NaN");
      if (Double.isInfinite(delta)) throw new IllegalArgumentException("absolute tolerance (delta) cannot be infinity");
      if (delta < 0) delta = Tolerance.getTolerance(); // use default system-wide tolerance
      this.delta = delta;
    }
    else this.delta = null; // no tolerance
  }

  @Override
  public void describeTo (Description description) {
    description.appendText("Data size: ").appendValue(expected.getSize())
               .appendText(" format: ").appendValue(expected.getFormat())
               .appendText(" value: ").appendValue(expected)
               ;
    if (delta != null) {
      description.appendText(" within absolute tolerance of ").appendValue(delta).appendText(" and type promotion");
    }
  }

  @Override
  public void describeMismatch (Object item, Description mismatchDescription) {
    if (item != null) {
      if (Data.class.isInstance(item)) {
        Data other = (Data) item;
        mismatchDescription.appendText("Data size: ").appendValue(other.getSize())
                           .appendText(" format: ").appendValue(other.getFormat())
                           .appendText(" value: ");
      }
      else {
        mismatchDescription.appendText(item.getClass().getName()).appendText(" ");
      }
    }
    mismatchDescription.appendValue(item);
  }

  @Override
  public boolean matches (Object item) {
    if (item == null || !Data.class.isInstance(item)) return false;
    if (delta != null) {
      int flags = Data.EQ_DEFAULT | Data.EQ_ABSTOL;
      return expected.equals((Data) item, flags, delta); // <-- tolerance AND *implicit* type promotion
    }
    else return expected.equals(item); // <-- no tolerance AND no type promotion
  }

  //////////////////////////////////////////////////////////////////////////////
  // Factory methods
  //////////////////////////////////////////////////////////////////////////////

  /**
   * Creates a matcher that matches when the examined Data object is logically equal to the specified
   * <code>expectedData</code> as determined by calling the {@link Data#equals(Data)} method on
   * the <b>examined</b> object without any type promotion.
   * @param expectedVal expected value
   * @return Matcher to examine actual against expected value with no type promotion
   */
  public static Matcher<Data> equalToData (Data expectedVal) {
    return new IsDataEqual<>(expectedVal, null);
  }

  /**
   * Creates a matcher that matches when the examined Data object is logically equal to the specified
   * <code>expectedData</code> as determined by calling the {@link Data#equals(Data, int, double)} method with zero
   * tolerance on the <b>examined</b> object with type promotion (e.g. type 'F', 'I', etc. will be promoted to
   * higher precision type via {@link Data#promoteType(byte, byte)}.
   * @param expectedVal expected value
   * @return Matcher to examine actual against expected value within specified delta with type promotion
   */
  public static Matcher<Data> equalToPromoteType (Data expectedVal) {
    return new IsDataEqual<>(expectedVal, 0.0);
  }

  /**
   * Creates a matcher that matches when the examined Data object is logically equal to the specified
   * <code>expectedData</code> as determined by calling the {@link Data#equals(Data, int, double)} method on
   * the <b>examined</b> object with type promotion (e.g. type 'F', 'I', etc. will be promoted to
   * higher precision type via {@link Data#promoteType(byte, byte)}.
   * @param expectedVal expected value
   * @param delta absolute tolerance to comparing values
   * @return Matcher to examine actual against expected value within specified delta with type promotion
   */
  public static Matcher<Data> equalToPromoteType (Data expectedVal, double delta) {
    return new IsDataEqual<>(expectedVal, delta);
  }

  //////////////////////////////////////////////////////////////////////////////
  // Below factory Matcher methods do no enforce static type checking, useful for checking
  // against generic Object references, such as with return value of Table.get(key)
  //////////////////////////////////////////////////////////////////////////////

  /**
   * Creates a matcher that does not enforce the values being compared to be of the same static type.
   * E.g. for comparing against any Object such as value returned from Table.get(key).
   * @param expectedVal expected value
   * @return Matcher to examine actual against expected value with no type promotion
   */
  public static Matcher<Object> equalToDataObj (Data expectedVal) {
    return new IsDataEqual<>(expectedVal);
  }

  /**
   * Creates a matcher that does not enforce the values being compared to be of the same static type.
   * E.g. for comparing against any Object such as value returned from Table.get(key).
   * @param expectedVal expected value
   * @return Matcher to examine actual against expected value with no type promotion
   */
  public static Matcher<Object> equalToDataObjPromoteType (Data expectedVal) {
    return new IsDataEqual<>(expectedVal);
  }

  /**
   * Creates a matcher that does not enforce the values being compared to be of the same static type.
   * If {@code expectedVal} is not a Data, it will use new Data(expectedObj) for expected value.
   * @param expectedVal expected value (Data or any Object that the {@link Data#Data(Object)} constructor supports.)
   * @return Matcher to examine actual against expected value with no type promotion
   */
  public static Matcher<Object> equalToObj (Object expectedVal) {
    return new IsDataEqual<>(expectedVal);
  }

  /**
   * Creates a matcher that does not enforce the values being compared to be of the same static type.
   * If {@code expectedVal} is not a Data, it will use new Data(expectedObj) for expected value.
   * @param expectedVal expected value (Data or any Object that the {@link Data#Data(Object)} constructor supports.)
   * @return Matcher to examine actual against expected value with type promotion
   */
  public static Matcher<Object> equalToObjPromoteType (Object expectedVal) {
    return new IsDataEqual<>(expectedVal, 0.0);
  }

}
