/**
 *  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.isis.applib.util;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.base.Objects.ToStringHelper;
import com.google.common.base.Splitter;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;

/**
 * This is a copy of ObjectContracts in isis' applib, enhanced to deal with "cross-type comparisons", eg
 * polymorphic associations such as task#object).
 */
public class ObjectContracts2 {

    //region > compare

    /**
     * Evaluates which of p and q is first.
     *
     * @deprecated - please be aware that this utility heavily uses reflection.  We don't actually intend to deprecate this method (it's useful while prototyping), but we wanted to bring this to your attention!
     * @param propertyNames - the property name or names, CSV format.  If multiple properties, use the {@link #compare(Object, Object, String...) varargs} overloaded version of this method.
     */
    @Deprecated
    @SuppressWarnings("unchecked")
    public static <T> int compare(final T p, final T q, final String propertyNames) {
        final Iterable<String> propertyNamesIter = csvToIterable(propertyNames);
        return compare(p, q, propertyNamesIter);
    }

    /**
     * Evaluates which of p and q is first.
     *
     * @deprecated - please be aware that this utility heavily uses reflection.  We don't actually intend to deprecate this method (it's useful while prototyping), but we wanted to bring this to your attention!
     */
    @Deprecated
    @SuppressWarnings("unchecked")
    public static <T> int compare(final T p, final T q, final String... propertyNames) {
        final Iterable<String> propertyNamesIter = varargsToIterable(propertyNames);
        return compare(p, q, propertyNamesIter);
    }

    private static <T> int compare(final T p, final T q, final Iterable<String> propertyNamesIter) {
        if(p == null) { return -1;}
        if(q == null) { return +1;}
        if(p.getClass() != q.getClass()) {
            // just sort on the class type
            return Ordering.natural().onResultOf(new Function<Object, String>() {
                @Override public String apply(final Object o) {
                    return o.getClass().getSimpleName();
                }
            }).compare(p, q);
        }

        final Iterable<Clause> clauses = clausesFor(propertyNamesIter);
        ComparisonChain chain = ComparisonChain.start();
        for (final Clause clause : clauses) {
            final Comparable<T> propertyValueOfP = (Comparable<T>) clause.getValueOf(p);
            final Comparable<T> propertyValueOfQ = (Comparable<T>) clause.getValueOf(q);
            chain = chain.compare(propertyValueOfP, propertyValueOfQ, clause.getDirection().getOrdering());
        }
        return chain.result();
    }
    //endregion

    //region > compareBy
    /**
     * Returns a {@link Comparator} to evaluate objects by their property name(s).
     * @deprecated - please be aware that this utility heavily uses reflection.  We don't actually intend to deprecate this method (it's useful while prototyping), but we wanted to bring this to your attention!
     * @param propertyNames - the property name or names, CSV format.  If multiple properties, use the {@link #compareBy(String...)} varargs} overloaded version of this method.
     */
    @Deprecated
    @SuppressWarnings("unchecked")
    public static <T> Comparator<T> compareBy(final String propertyNames){
        return new Comparator<T>() {
            @Override
            public int compare(T p, T q) {
                return ObjectContracts.compare(p, q, propertyNames);
            }
        };
    }
    /**
     * Returns a {@link Comparator} to evaluate objects by their property name(s).
     * @deprecated - please be aware that this utility heavily uses reflection.  We don't actually intend to deprecate this method (it's useful while prototyping), but we wanted to bring this to your attention!
     */
    @Deprecated
    @SuppressWarnings("unchecked")
    public static <T> Comparator<T> compareBy(final String... propertyNames){
        return new Comparator<T>() {
            @Override
            public int compare(T p, T q) {
                return ObjectContracts.compare(p, q, propertyNames);
            }
        };
    }
    //endregion

    //region > toString

    /**
     * Returns a string representation of the object consisting of the specified property name(s).
     * @deprecated - please be aware that this utility heavily uses reflection.  We don't actually intend to deprecate this method (it's useful while prototyping), but we wanted to bring this to your attention!
     * @param propertyNames - the property name or names, CSV format.  If multiple properties, use the {@link #toString(Object, String...)} varargs} overloaded version of this method.
     */
    @Deprecated
    public static String toString(Object p, String propertyNames) {
        return new ObjectContracts().toStringOf(p, propertyNames);
    }
    /**
     * Returns a string representation of the object consisting of the specified property name(s).
     * @deprecated - please be aware that this utility heavily uses reflection.  We don't actually intend to deprecate this method (it's useful while prototyping), but we wanted to bring this to your attention!
     */
    @Deprecated
    public static String toString(Object p, String... propertyNames) {
        return new ObjectContracts().toStringOf(p, propertyNames);
    }
    //endregion

    //region > hashCode
    /**
     * Returns the hashCode for the object using the specified property name(s).
     * @deprecated - please be aware that this utility heavily uses reflection.  We don't actually intend to deprecate this method (it's useful while prototyping), but we wanted to bring this to your attention!
     * @param propertyNames - the property name or names, CSV format.  If multiple properties, use the {@link #hashCode(Object, String...)} varargs} overloaded version of this method.
     */
    @Deprecated
    public static int hashCode(Object obj, String propertyNames) {
        final Iterable<String> propertyNamesIter = csvToIterable(propertyNames);
        return hashCode(obj, propertyNamesIter);
    }

    /**
     * Returns the hashCode for the object using the specified property name(s).
     * @deprecated - please be aware that this utility heavily uses reflection.  We don't actually intend to deprecate this method (it's useful while prototyping), but we wanted to bring this to your attention!
     */
    @Deprecated
    public static int hashCode(Object obj, String... propertyNames) {
        final Iterable<String> propertyNamesIter = varargsToIterable(propertyNames);
        return hashCode(obj, propertyNamesIter);
    }

    private static int hashCode(final Object obj, final Iterable<String> propertyNamesIter) {
        final List<Object> propertyValues = Lists.newArrayList();
        for (final Clause clause : clausesFor(propertyNamesIter)) {
            final Object propertyValue = clause.getValueOf(obj);
            if(propertyValue != null) {
                propertyValues.add(propertyValue);
            }
        }
        return Objects.hashCode(propertyValues.toArray());
    }
    //endregion

    //region > equals

    /**
     * Returns whether two objects are equal, considering just the specified property name(s).
     * @deprecated - please be aware that this utility heavily uses reflection.  We don't actually intend to deprecate this method (it's useful while prototyping), but we wanted to bring this to your attention!
     * @param propertyNames - the property name or names, CSV format.  If multiple properties, use the {@link #equals(Object, Object, String...)} varargs} overloaded version of this method.
     */
    @Deprecated
    public static boolean equals(Object p, Object q, String propertyNames) {
        if(p==null && q==null) {
            return true;
        }
        if(p==null || q==null) {
            return false;
        }
        if(p.getClass() != q.getClass()) {
            return false;
        }
        final Iterable<String> propertyNamesIter = csvToIterable(propertyNames);
        return equals(p, q, propertyNamesIter);
    }

    /**
     * Returns whether two objects are equal, considering just the specified property name(s).
     * @deprecated - please be aware that this utility heavily uses reflection.  We don't actually intend to deprecate this method (it's useful while prototyping), but we wanted to bring this to your attention!
     */
    @Deprecated
    public static boolean equals(Object p, Object q, String... propertyNames) {
        if(p==null && q==null) {
            return true;
        }
        if(p==null || q==null) {
            return false;
        }
        if(p.getClass() != q.getClass()) {
            return false;
        }
        final Iterable<String> propertyNamesIter = varargsToIterable(propertyNames);
        return equals(p, q, propertyNamesIter);
    }

    private static boolean equals(final Object p, final Object q, final Iterable<String> propertyNamesIter) {
        final Iterable<Clause> clauses = clausesFor(propertyNamesIter);
        for (final Clause clause : clauses) {
            final Object pValue = clause.getValueOf(p);
            final Object qValue = clause.getValueOf(q);
            if(!Objects.equal(pValue, qValue)) {
                return false;
            }
        }
        return true;
    }
    //endregion

    //region > helpers
    private static Iterable<Clause> clausesFor(final Iterable<String> iterable) {
        return Iterables.transform(iterable, new Function<String, Clause>() {
            @Override
            public Clause apply(String input) {
                return Clause.parse(input);
            }
        });
    }

    private static Iterable<String> csvToIterable(final String propertyNames) {
        return Splitter.on(',').split(propertyNames);
    }

    private static List<String> varargsToIterable(final String[] iterable) {
        return Arrays.asList(iterable);
    }
    //endregion

    //region > toStringOf

    private final List<ObjectContracts.ToStringEvaluator> evaluators = Lists.newArrayList();
    public ObjectContracts2 with(ObjectContracts.ToStringEvaluator evaluator) {
        evaluators.add(evaluator);
        return this;
    }

    /**
     * Returns a string representation of two objects, considering just the specified property name(s).
     * @deprecated - please be aware that this utility heavily uses reflection.  We don't actually intend to deprecate this method (it's useful while prototyping), but we wanted to bring this to your attention!
     * @param propertyNames - the property name or names, CSV format.  If multiple properties, use the {@link #toString(Object, String...)} varargs} overloaded version of this method.
     */
    @Deprecated
    public String toStringOf(Object p, String propertyNames) {
        final Iterable<String> propertyNamesIter = csvToIterable(propertyNames);
        return toStringOf(p, propertyNamesIter);
    }

    /**
     * Returns a string representation of two objects, considering just the specified property name(s).
     * @deprecated - please be aware that this utility heavily uses reflection.  We don't actually intend to deprecate this method (it's useful while prototyping), but we wanted to bring this to your attention!
     */
    @Deprecated
    public String toStringOf(Object p, String... propertyNames) {
        final Iterable<String> propertyNamesIter = varargsToIterable(propertyNames);
        return toStringOf(p, propertyNamesIter);
    }

    private String toStringOf(final Object p, final Iterable<String> propertyNamesIter) {
        final ToStringHelper stringHelper = Objects.toStringHelper(p);
        for (final Clause clause : clausesFor(propertyNamesIter)) {
            stringHelper.add(clause.getPropertyName(), asString(clause, p));
        }
        return stringHelper.toString();
    }

    private String asString(final Clause clause, Object p) {
        final Object value = clause.getValueOf(p);
        if(value == null) {
            return null;
        }
        for (ObjectContracts.ToStringEvaluator evaluator : evaluators) {
            if(evaluator.canEvaluate(value)) {
                return evaluator.evaluate(value);
            }
        }
        return value.toString();
    }

    //endregion


}
