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;
import de.renew.util.Value;


/**
 * A {@code GuardExpression} is an {@code Expression} that only evaluates successfully if a given {@code Function}
 * returns {@code true} when passed the value of another {@code Expression}. Otherwise, it throws {@link Impossible}
 * during evaluation.
 */
public class GuardExpression implements Expression {
    /**
     * The {@code Expression} whose value is passed as an argument to the function during evaluation.
     */
    private final Expression _argument;
    /**
     * The {@code Function} whose return value is checked for being {@code true} during evaluation.
     */
    private final Function _function;

    /**
     * Constructs a new {@code GuardExpression} based on an argument {@code Expression} and the {@code Function} that
     * will be applied to it.
     *
     * @param argument the {@code Expression} whose value is passed as an argument to {@code function} when the
     *                 {@code GuardExpression} is evaluated.
     * @param function the {@code Function} that has to return {@code true} when passed the value of {@code argument}
     *                 for the {@code GuardExpression} to evaluate successfully.
     */
    public GuardExpression(Expression argument, Function function) {
        _argument = argument;
        _function = function;
    }

    @Override
    public boolean isInvertible() {
        return _argument.isInvertible();
    }

    @Override
    public Class<?> getType() {
        return _argument.getType();
    }

    /**
     * Gets the {@code Expression} whose value is passed as an argument to the function during evaluation.
     * 
     * @return the argument {@code Expression}
     */
    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);

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

        Notifiable sourceListener = new Notifiable() {
            @Override
            public void boundNotify(IStateRecorder irecorder) throws Impossible {
                if (Unify.isBound(source)) {
                    Object result = _function.function(source.getValue());
                    if (result instanceof Value) {
                        result = ((Value) result).value;
                    }
                    if (!Boolean.TRUE.equals(result)) {
                        throw new Impossible();
                    }
                }
            }
        };
        source.addListener(sourceListener, recorder);

        return source.getValue();
    }

    @Override
    public Object registerCalculation(
        VariableMapper mapper, IStateRecorder recorder, ICalculationChecker checker)
        throws Impossible
    {
        return _argument.registerCalculation(mapper, recorder, checker);
    }

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