/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.generic;

import java.io.Serializable;
import java.util.Comparator;
import java.util.Map;
import java.util.Objects;
import java.util.function.Predicate;

import lombok.experimental.UtilityClass;
import net.ranides.assira.collection.IntComparator;
import net.ranides.assira.collection.maps.WeakIdentMap;
import net.ranides.assira.text.LexicalCast;


/**
 * Static methods for comparing objects, comparisons are null-safe.
 */
@SuppressWarnings({
    "PMD.ShortVar", 
    "PMD.ShortMethodName"
})
@UtilityClass
public class CompareUtils {

    @SuppressWarnings({"rawtypes"})
    private static final Comparator AUTO_COMPARATOR = new NaturalComparator();

    /**
     * Compares passed arguments usig Object#equals method.
     *
     * Method is null-safe.
     *
     * @param a a
     * @param b b
     * @return bool
     */
    @SuppressWarnings("PMD")
    public static boolean equals(Object a, Object b) {
        return Objects.equals(a, b);
    }

    /**
     * If both arguments are numbers ({@code instanceof Number}) then compares
     * with specified precision. Use standard comparison otherwise.
     *
     * Method is null-safe.
     *
     * @param a a
     * @param b b
     * @param precision precision
     * @return bool
     */
    public static boolean equals(Object a, Object b, double precision) {
        if(a instanceof Number && b instanceof Number) {
            return equals((Number)a, (Number)b, precision);
        } else {
            return CompareUtils.equals(a, b);
        }
    }
    
    /**
     * Compares numbers with specified precision.
     * Method is null-safe.
     *
     * @param a a
     * @param b b
     * @param precision precision
     * @return bool
     */
    public static boolean equals(Number a, Number b, double precision) {
        return (null==a) ? (null==b) : (null!=b) && (Math.abs(a.doubleValue() - b.doubleValue()) <= precision);
    }

    /**
     * Compares two values using natural comparator.
     *
     * Method is null-safe.
     *
     * @param value1 value1
     * @param value2 value2
     * @param <T> T
     * @return int
     */
    @SuppressWarnings("unchecked")
    public static <T> int cmp(T value1, T value2) {
        return AUTO_COMPARATOR.compare(value1, value2);
    }

    /**
     * Compares two integer values.
     *
     * @param value1 value1
     * @param value2 value2
     * @return int
     */
    public static int cmp(int value1, int value2) {
        return value1 - value2;
    }

    /**
     * Compares two values using provided comparator.
     * Comparator is optional.
     * Method is null-safe only if provided comparator supports null values.
     *
     * @param comparator comparator
     * @param value1 value1
     * @param value2 value2
     * @param <T> T
     * @return int
     */
    public static <T> int cmp(Comparator<? super T> comparator, T value1, T value2) {
        if(null != comparator ) {
            return comparator.compare(value1, value2);
        } else {
            return cmp(value1, value2);
        }
    }

    /**
     * Compares two values using provided comparator.
     * Comparator is optional.
     *
     * @param comparator comparator
     * @param value1 value1
     * @param value2 value2
     * @return int
     */
    public static int cmp(IntComparator comparator, int value1, int value2) {
        if(null != comparator ) {
            return comparator.compare(value1, value2);
        } else {
            return (value1 - value2);
        }
        
    }

    /**
     * Returns lower non-null value.
     * Uses provided comparator for comparing objects.
     *
     * Method is null-safe.
     *
     * @param comparator comparator
     * @param value1 value1
     * @param value2 value2
     * @param <T> T
     * @return value
     */
    public static <T> T lower(Comparator<? super T> comparator, T value1, T value2) {
        if(null == value1) {
            return value2;
        }
        if(null == value2) {
            return value1;
        }
        return cmp(comparator, value1, value2) < 0 ? value1 : value2;
    }

    /**
     * Returns lower int value.
     * Uses provided comparator for comparing objects.
     *
     * @param comparator comparator
     * @param value1 value1
     * @param value2 value2
     * @return value
     */
    public static int lower(IntComparator comparator, int value1, int value2) {
        return cmp(comparator, value1, value2) < 0 ? value1 : value2;
    }

    /**
     * Returns higher non-null value.
     * Uses provided comparator for comparing objects.
     *
     * Method is null-safe.
     *
     * @param comparator comparator
     * @param value1 value1
     * @param value2 value2
     * @param <T> T
     * @return value
     */
    public static <T> T higher(Comparator<? super T> comparator, T value1, T value2) {
        if(null == value1) {
            return value2;
        }
        if(null == value2) {
            return value1;
        }
        return cmp(comparator, value1, value2) > 0 ? value1 : value2;
    }

    /**
     * Returns higher int value.
     * Uses provided comparator for comparing objects.
     *
     * @param comparator comparator
     * @param value1 value1
     * @param value2 value2
     * @return value
     */
    public static int higher(IntComparator comparator, int value1, int value2) {
        return cmp(comparator, value1, value2) > 0 ? value1 : value2;
    }

    /**
     * Returns predicate which returns true if tested object is equal to provided one.
     *
     * Method is null-safe.
     *
     * @param a a
     * @param <T> T
     * @return predicate
     */
    public static <T> Predicate<T> predicate(T a) {
        if(null == a) {
            return b -> b==null;
        } else {
            return b -> a.equals(b);
        }
    }

    /**
     * Returns natural comparator.
     * Comparator assumes that both objects are Comparable.
     * It uses Comparable interface if objects are compatible.
     * If not, it uses LexicalCast to normalize types.
     *
     * @param <T> T
     * @return comparator
     */
    @SuppressWarnings("unchecked")
    public static <T> Comparator<T> comparator() {
        return AUTO_COMPARATOR;
    }

    /**
     * Returns natural comparator for provided type.
     * It could return optimized version for some types, for example for "int".
     *
     * @param type type
     * @param <T> T
     * @return comparator
     */
    @SuppressWarnings("unchecked")
    public static <T> Comparator<T> comparator(Class<?> type) {
        if(int.class.equals(type) || Integer.class.equals(type)) {
            return (Comparator)IntComparator.ASC;
        } else {
            return AUTO_COMPARATOR;
        }
    }

    /**
     * Returns reversed comparator
     *
     * @param comparator comparator
     * @param <T> T
     * @return comparator
     */
    public static <T> Comparator<T> reversed(Comparator<T> comparator) {
        return comparator.reversed();
    }

    /**
     * Identity comparator is quite strange in that sense, that orders objects in unpredictable, but stable way.
     *
     * Two objects are compared using identityHashCode first.
     * If two objects have the same hashCode, then their order is undefined, but stable as long as
     * you use the same comparator
     *
     * That means: for two equal objects "a" and "b" result of comparison won't change in time and:
     * COMPARATOR.compare(a,b) = -COMPARATOR.compare(b,a) != 0
     *
     * This comparator is useful, if you want to store duplicates in standard TreeSets and TreeMaps
     *
     * @param <T> T
     * @return comparator
     */
    public static <T> Comparator<T> identity() {
        return new IdentComparator<>();
    }

    private static final class IdentComparator<T> implements Comparator<T>, Serializable {

        private final Map<T, Integer> map = new WeakIdentMap<>();

        private int counter = 1;

        @Override
        public int compare(T a, T b) {
            int diff = Integer.compare(System.identityHashCode(a), System.identityHashCode(b));
            if(diff != 0) {
                return 0;
            }
            int ai = map.computeIfAbsent(a, k -> ++counter);
            int bi = map.computeIfAbsent(b, k -> ++counter);
            return Integer.compare(ai, bi);
        }
    }
    
    private static final class NaturalComparator implements Comparator<Comparable>, Serializable {
        
        private static final long serialVersionUID = 1L;
        
        @SuppressWarnings("unchecked")
        @Override
        public int compare(Comparable a, Comparable b) {
            if (a == null) {
                return (b == null ? 0 : -1);
            }
            if (b == null) {
                return +1;
            }
            if (a.getClass().isInstance(b)) {
                return a.compareTo(b);
            }
            if (b.getClass().isInstance(a)) {
                return -b.compareTo(a);
            }
            throw new ClassCastException("Values are not Comparable: " + a + " or " + b);
        }
    }

}
