package de.renew.expression;

import de.renew.unify.ICalculationChecker;
import de.renew.unify.IStateRecorder;
import de.renew.unify.Impossible;
import de.renew.unify.Notifiable;
import de.renew.unify.Unify;
import de.renew.unify.Variable;


/**
 * A {@code CallExpression} is an {@code Expression} that represents the application of a function to the value of
 * another {@code Expression}.
 */
public class CallExpression extends ExpressionWithTypeField {
    /**
     * The {@code Expression} whose value will be used as the argument of the function.
     * Multiple arguments are modelled by an expression that returns
     * a tuple that is processed by a function.
     */
    private final Expression _argument;
    /**
     * The {@code Function} that is called during evaluation.
     */
    private final Function _function;

    /**
     * Constructs a new {@code CallExpression} based on a {@code Class}, a {@code Function} and the {@code Expression}
     * that evaluates to the {@code Function}'s argument.
     *
     * @param targetType the type of the {@code CallExpression}'s value
     * @param argument the {@code Expression} on whose value {@code function} is called during evaluation
     * @param function the {@code Function} that the {@code CallExpression} calls during evaluation
     */
    public CallExpression(Class<?> targetType, Expression argument, Function function) {
        super(targetType);
        _argument = argument;
        _function = function;
    }

    @Override
    public boolean isInvertible() {
        return false;
    }

    /**
     * Gets the {@code Expression} that evaluates to the called function's argument.
     *
     * @return the {@code Expression} that evaluates to the called function's argument
     */
    public Expression getArgument() {
        return _argument;
    }

    @Override
    public Object startEvaluation(
        VariableMapper mapper, IStateRecorder recorder, ICalculationChecker checker)
        throws Impossible
    {
        final Variable source =
            new Variable(_argument.startEvaluation(mapper, recorder, checker), recorder);
        final Variable target = new Variable();

        if (checker != null) {
            checker.addEarlyVariable(source, recorder);
        }

        expressionConstraint(target, _function, source, recorder);

        return target.getValue();
    }

    @Override
    public Object registerCalculation(
        VariableMapper mapper, IStateRecorder recorder, ICalculationChecker checker)
        throws Impossible
    {
        Variable source =
            new Variable(_argument.registerCalculation(mapper, recorder, checker), recorder);
        Variable target = new Variable();

        checker.addLateVariable(source, recorder);
        checker.addCalculated(getType(), target, source.getValue(), recorder);

        return target.getValue();
    }

    /**
     * Ensures that {@code target} is unified with {@code function(source)} when {@code source} is bound.
     *
     * @param target the {@code Variable} that should be unified with the result of {@code function} applied to
     *               the value of {@code source}
     * @param function the {@code Function} that is applied to the value of {@code source}
     * @param source the variable that represents the argument to {@code function}
     * @param recorder the {@code IStateRecorder} used to add a listener to {@code source}
     * @throws Impossible if adding a listener to {@code source} fails
     */
    public static void expressionConstraint(
        final Variable target, final Function function, final Variable source,
        IStateRecorder recorder) throws Impossible
    {
        // Create a listener.
        Notifiable listener = new Notifiable() {
            @Override
            public void boundNotify(IStateRecorder irecorder) throws Impossible {
                if (Unify.isBound(source)) {
                    Unify.unify(target, function.function(source.getValue()), irecorder);
                }
            }
        };
        source.addListener(listener, recorder);
    }

    @Override
    public String toString() {
        return "CallExpr(" + de.renew.util.Types.typeToString(getType()) + ": " + _function + ", "
            + _argument + ")";
    }
}