/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.sis.util;


/**
 * Specifies the level of strictness when comparing two {@link LenientComparable} objects
 * for equality. This enumeration allows users to specify which kind of differences can be
 * tolerated between two objects: differences in implementation class, differences in
 * some kinds of property, or slight difference in numerical values.
 *
 * <p>This enumeration is <em>ordered</em> from stricter to more lenient levels:</p>
 *
 * <ol>
 *   <li>{@link #STRICT}          – All attributes of the compared objects shall be strictly equal.</li>
 *   <li>{@link #BY_CONTRACT}     – Only the attributes published in the interface contract need to be compared.</li>
 *   <li>{@link #IGNORE_METADATA} – Only the attributes relevant to the object functionality are compared.</li>
 *   <li>{@link #APPROXIMATE}     – Only the attributes relevant to the object functionality are compared,
 *                                  with some tolerance threshold on numerical values.</li>
 *   <li>{@link #ALLOW_VARIANT}   – For objects not really equal but related (e.g. CRS using different axis order).</li>
 *   <li>{@link #DEBUG}           – Special mode for figuring out why two objects expected to be equal are not.</li>
 * </ol>
 *
 * If two objects are equal at some level of strictness <var>E</var>, then they should also
 * be equal at all levels listed below <var>E</var> in the above list. For example if two objects
 * are equal at the {@link #BY_CONTRACT} level, then they should also be equal at the
 * {@link #IGNORE_METADATA} level but not necessarily at the {@link #STRICT} level.
 *
 * @author  Martin Desruisseaux (Geomatys)
 * @version 1.0
 *
 * @see LenientComparable#equals(Object, ComparisonMode)
 * @see Utilities#deepEquals(Object, Object, ComparisonMode)
 *
 * @since 0.3
 */
public enum ComparisonMode {
    /**
     * All attributes of the compared objects shall be strictly equal. This comparison mode
     * is equivalent to the {@link Object#equals(Object)} method, and must be compliant with
     * the contract documented in that method. In particular, this comparison mode shall be
     * consistent with {@link Object#hashCode()} and be symmetric ({@code A.equals(B)} implies
     * {@code B.equals(A)}).
     *
     * <h4>Implementation note</h4>
     * In the SIS implementation, this comparison mode usually have the following
     * characteristics (not always, this is only typical):
     *
     * <ul>
     *   <li>The objects being compared need to be the same implementation class.</li>
     *   <li>Private fields are compared directly instead of invoking public getter methods.</li>
     * </ul>
     *
     * @see Object#equals(Object)
     */
    STRICT,

    /**
     * Only the attributes published in some contract (typically a GeoAPI interface) need to be compared.
     * The implementation classes do not need to be the same and some private attributes may be ignored.
     *
     * <p>Note that this comparison mode does <strong>not</strong> guaranteed {@link Object#hashCode()}
     * consistency, neither comparison symmetry (i.e. {@code A.equals(B)} and {@code B.equals(A)} may
     * return different results if the {@code equals} methods are implemented differently).</p>
     *
     * <h4>Implementation note</h4>
     * In the SIS implementation, this comparison mode usually have the following
     * characteristics (not always, this is only typical):
     *
     * <ul>
     *   <li>The objects being compared need to implement the same GeoAPI interfaces.</li>
     *   <li>Public getter methods are used (no direct access to private fields).</li>
     * </ul>
     */
    BY_CONTRACT,

    /**
     * Only the attributes relevant to the object functionality are compared. Attributes that
     * are only informative can be ignored. This comparison mode is typically less strict than
     * {@link #BY_CONTRACT}.
     *
     * <h4>Application to coordinate reference systems</h4>
     * If the objects being compared are {@link org.opengis.referencing.crs.CoordinateReferenceSystem} instances,
     * then only the properties relevant to the coordinate localization shall be compared.
     * Metadata like the {@linkplain org.apache.sis.referencing.crs.AbstractCRS#getIdentifiers() identifiers}
     * or the {@linkplain org.apache.sis.referencing.crs.AbstractCRS#getDomainOfValidity() domain of validity},
     * which have no impact on the coordinates being calculated, shall be ignored.
     *
     * <h4>Application to coordinate operations</h4>
     * If the objects being compared are {@link org.opengis.referencing.operation.MathTransform} instances,
     * then two transforms defined in a different way may be considered equivalent. For example, it is possible
     * to define a Mercator projection in different ways, named "variant A", "variant B" and "variant C"
     * in EPSG dataset, each having their own set of parameters.
     * The {@link #STRICT} or {@link #BY_CONTRACT} modes shall consider two projections as equal only if their
     * {@linkplain org.apache.sis.referencing.operation.transform.AbstractMathTransform#getParameterValues()
     * parameter values} are strictly identical, while the {@code IGNORE_METADATA} mode can consider
     * those objects as equivalent despite difference in the set of parameters, as long as coordinate
     * transformations still produce the same results.
     *
     * <h4>Example</h4>
     * A <cite>"Mercator (variant B)"</cite> projection with a <cite>standard parallel</cite> value of 60° produces the
     * same results than a <cite>"Mercator (variant A)"</cite> projection with a <cite>scale factor</cite> value of 0.5.
     *
     * @see org.apache.sis.util.Utilities#equalsIgnoreMetadata(Object, Object)
     */
    IGNORE_METADATA,

    /**
     * Only the attributes relevant to the object functionality are compared, with some tolerance
     * threshold on numerical values.
     *
     * <h4>Application to coordinate operations</h4>
     * If two {@link org.opengis.referencing.operation.MathTransform} objects are considered equal according this mode,
     * then for any given identical source position, the two compared transforms shall compute at least approximately
     * the same target position.
     * A small difference is tolerated between the target coordinates calculated by the two math transforms.
     * How small is “small” is implementation dependent — the threshold cannot be specified in the current
     * implementation, because of the non-linear nature of map projections.
     *
     * @since 1.0
     */
    APPROXIMATE,

    /**
     * Most but not all attributes relevant to the object functionality are compared.
     * This comparison mode is equivalent to {@link #APPROXIMATE}, except that it
     * ignores some attributes that may differ between objects not equal but related.
     *
     * <p>The main purpose of this method is to verify if two Coordinate Reference Systems (CRS)
     * are approximately equal ignoring axis order.</p>
     *
     * <h4>Example</h4>
     * Consider two geographic coordinate reference systems with the same attributes except axis order,
     * where one CRS uses (<var>latitude</var>, <var>longitude</var>) axes
     * and the other CRS uses (<var>longitude</var>, <var>latitude</var>) axes.
     * All comparison modes (even {@code APPROXIMATE}) will consider those two CRS as different,
     * except this {@code ALLOW_VARIANT} mode which will consider one CRS to be a variant of the other.
     *
     * @since 0.7
     */
    ALLOW_VARIANT,

    /**
     * Same as {@link #APPROXIMATE}, except that an {@link AssertionError} is thrown if the two
     * objects are not equal and assertions are enabled. The exception message and stack trace help
     * to locate which attributes are not equal. This mode is typically used in assertions like below:
     *
     * {@snippet lang="java" :
     *     assert Utilities.deepEquals(object1, object2, ComparisonMode.DEBUG);
     *     }
     *
     * Note that a comparison in {@code DEBUG} mode may still return {@code false} without
     * throwing an exception, since not all corner cases are tested. The exception is only
     * intended to provide more details for some common cases.
     */
    @Debug
    DEBUG;

    /**
     * Returns {@code true} if this comparison ignores metadata.
     * This method currently returns {@code true} for {@code IGNORE_METADATA}, {@code APPROXIMATE}
     * or {@code DEBUG} only, but this list may be extended in future SIS versions.
     *
     * @return whether this comparison ignore metadata.
     *
     * @since 0.6
     */
    public boolean isIgnoringMetadata() {
        return ordinal() >= IGNORE_METADATA.ordinal();
    }

    /**
     * Returns {@code true} if this comparison uses a tolerance threshold.
     * This method currently returns {@code true} for {@code APPROXIMATE} or {@code DEBUG} only,
     * but this list may be extended in future SIS versions.
     *
     * @return whether this comparison uses a tolerance threshold.
     *
     * @since 1.0
     */
    public boolean isApproximate() {
        return ordinal() >= APPROXIMATE.ordinal();
    }

    /**
     * If the two given objects are equal according one of the modes enumerated in this class,
     * then returns that mode. Otherwise returns {@code null}.
     *
     * <p><b>Note:</b> this method never return the {@link #DEBUG} mode.</p>
     *
     * @param  o1  the first object to compare, or {@code null}.
     * @param  o2  the second object to compare, or {@code null}.
     * @return the most suitable comparison mode, or {@code null} if the two given objects
     *         are not equal according any mode in this enumeration.
     */
    public static ComparisonMode equalityLevel(final Object o1, Object o2) {
        if (o1 == o2) {
            return STRICT;
        }
        if (o1 != null && o2 != null) {
            if (o1.equals(o2)) {
                return STRICT;
            }
            final LenientComparable cp;
            if (o1 instanceof LenientComparable) {
                cp = (LenientComparable) o1;
            } else if (o2 instanceof LenientComparable) {
                cp = (LenientComparable) o2;
                o2 = o1;
            } else {
                return null;
            }
            if (cp.equals(o2, BY_CONTRACT))     return BY_CONTRACT;
            if (cp.equals(o2, IGNORE_METADATA)) return IGNORE_METADATA;
            if (cp.equals(o2, APPROXIMATE))     return APPROXIMATE;
            if (cp.equals(o2, ALLOW_VARIANT))   return ALLOW_VARIANT;
        }
        return null;
    }
}
