/*
 * 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.filter;

import java.util.List;
import java.util.Collection;
import java.util.Objects;
import java.math.BigInteger;
import java.math.BigDecimal;
import org.apache.sis.util.Numbers;
import org.apache.sis.math.Fraction;
import org.apache.sis.math.DecimalFunctions;
import org.apache.sis.filter.internal.Node;


/**
 * Base class for expressions, comparators or filters performing operations on two expressions.
 * The nature of the operation depends on the subclass. If operands are numerical values, they
 * may be converted to a common type before the operation is performed. That operation is not
 * necessarily an arithmetic operation; it may be a comparison for example.
 *
 * @author  Johann Sorel (Geomatys)
 * @author  Martin Desruisseaux (Geomatys)
 *
 * @param  <R>   the type of resources (e.g. {@code Feature}) used as inputs.
 * @param  <V1>  the type of value computed by the first expression.
 * @param  <V2>  the type of value computed by the second expression.
 */
abstract class BinaryFunction<R,V1,V2> extends Node {
    /**
     * For cross-version compatibility.
     */
    private static final long serialVersionUID = -8632475810190545852L;

    /**
     * The first of the two expressions to be used by this function.
     *
     * @see #getExpression1()
     */
    @SuppressWarnings("serial")         // Most SIS implementations are serializable.
    protected final Expression<R, ? extends V1> expression1;

    /**
     * The second of the two expressions to be used by this function.
     *
     * @see #getExpression2()
     */
    @SuppressWarnings("serial")         // Most SIS implementations are serializable.
    protected final Expression<R, ? extends V2> expression2;

    /**
     * Creates a new binary function.
     *
     * @param  expression1  the first of the two expressions to be used by this function.
     * @param  expression2  the second of the two expressions to be used by this function.
     */
    protected BinaryFunction(final Expression<R, ? extends V1> expression1,
                             final Expression<R, ? extends V2> expression2)
    {
        this.expression1 = Objects.requireNonNull(expression1);
        this.expression2 = Objects.requireNonNull(expression2);
    }

    /**
     * Returns the class of resources expected by this filter.
     * Defined for {@link Filter#getResourceClass()} and {@link Expression#getResourceClass()} implementations.
     *
     * @return type of resources accepted by this filter, or {@code null} if inconsistent.
     */
    public final Class<? super R> getResourceClass() {
        return specializedClass(expression1.getResourceClass(),
                                expression2.getResourceClass());
    }

    /**
     * Returns the expressions used as parameters by this function.
     * Defined for {@link Expression#getParameters()} implementations.
     */
    public final List<Expression<R,?>> getParameters() {
        return getExpressions();
    }

    /**
     * Returns the two expressions used as parameters by this filter.
     * Defined for {@link Filter#getExpressions()} implementations.
     *
     * @return a list of size 2 containing the two expressions.
     */
    public List<Expression<R,?>> getExpressions() {
        return List.of(expression1, expression2);
    }

    /**
     * Returns the two expressions in a list of size 2.
     * This is used for {@link #toString()}, {@link #hashCode()} and {@link #equals(Object)} implementations.
     */
    @Override
    protected final Collection<?> getChildren() {
        return getExpressions();
    }

    /**
     * Evaluates the expression for producing a result of numeric type.
     * This method delegates to one of the {@code applyAs(…)} methods.
     * If no {@code applyAs(…)} implementations can return null values,
     * this this method never return {@code null}.
     *
     * @param  left   the left operand. Cannot be null.
     * @param  right  the right operand. Cannot be null.
     * @return result of this function applied on the two given operands.
     *         May be {@code null} only if an {@code applyAs(…)} implementation returned a null value.
     */
    protected final Number apply(final Number left, final Number right) {
        final int type = Math.max(Numbers.getEnumConstant(left.getClass()),
                                  Numbers.getEnumConstant(right.getClass()));
        try {
            switch (type) {
                case Numbers.BIG_DECIMAL: {
                    return applyAsDecimal(Numbers.cast(left,  BigDecimal.class),
                                          Numbers.cast(right, BigDecimal.class));
                }
                case Numbers.BIG_INTEGER: {
                    return applyAsInteger(Numbers.cast(left,  BigInteger.class),
                                          Numbers.cast(right, BigInteger.class));
                }
                case Numbers.FRACTION: {
                    return applyAsFraction(Numbers.cast(left,  Fraction.class),
                                           Numbers.cast(right, Fraction.class));
                }
                case Numbers.LONG:
                case Numbers.INTEGER:
                case Numbers.SHORT:
                case Numbers.BYTE: {
                    return applyAsLong(left.longValue(), right.longValue());
                }
            }
        } catch (IllegalArgumentException | ArithmeticException e) {
            /*
             * Integer overflow, or division by zero, or attempt to convert NaN or infinity
             * to `BigDecimal`, or division does not have a terminating decimal expansion.
             */
            warning(e, true);
        }
        return applyAsDouble((left  instanceof Float) ? DecimalFunctions.floatToDouble((Float) left)  : left.doubleValue(),
                             (right instanceof Float) ? DecimalFunctions.floatToDouble((Float) right) : right.doubleValue());
    }

    /**
     * Calculates this function using given operands of {@code long} primitive type. If this function is a filter,
     * then this method should returns an {@link Integer} value 0 or 1 for false or true respectively.
     * Otherwise the result is usually a {@link Long}, except for division which may produce other types.
     * This method may return {@code null} if the operation cannot apply on numbers.
     *
     * @throws ArithmeticException if the operation overflows or if there is a division by zero.
     */
    protected Number applyAsLong(long left, long right) {
        return null;
    }

    /**
     * Calculates this function using given operands of {@code double} primitive type. If this function is a filter,
     * then this method should returns an {@link Integer} value 0 or 1 for false or true respectively.
     * Otherwise the result is usually a {@link Double}.
     * This method may return {@code null} if the operation cannot apply on numbers.
     */
    protected Number applyAsDouble(double left, double right) {
        return null;
    }

    /**
     * Calculates this function using given operands of {@code Fraction} type. If this function is a filter,
     * then this method should returns an {@link Integer} value 0 or 1 for false or true respectively.
     * Otherwise the result is usually a {@link Fraction}.
     * This method may return {@code null} if the operation cannot apply on numbers.
     *
     * @throws ArithmeticException if the operation overflows or if there is a division by zero.
     */
    protected Number applyAsFraction(Fraction left, Fraction right) {
        return null;
    }

    /**
     * Calculates this function using given operands of {@code BigInteger} type. If this function is a filter,
     * then this method should returns an {@link Integer} value 0 or 1 for false or true respectively.
     * Otherwise the result is usually a {@link BigInteger}, except for division which may produce other types.
     * This method may return {@code null} if the operation cannot apply on numbers.
     *
     * @throws ArithmeticException if there is a division by zero.
     */
    protected Number applyAsInteger(BigInteger left, BigInteger right) {
        return null;
    }

    /**
     * Calculates this function using given operands of {@code BigDecimal} type. If this function is a filter,
     * then this method should returns an {@link Integer} value 0 or 1 for false or true respectively.
     * Otherwise the result is usually a {@link BigDecimal}.
     * This method may return {@code null} if the operation cannot apply on numbers.
     *
     * @throws ArithmeticException if a division does not have a terminating decimal expansion.
     */
    protected Number applyAsDecimal(BigDecimal left, BigDecimal right) {
        return null;
    }
}
