package com.atlassian.bitbucket.mesh.util;

import com.google.common.primitives.Ints;
import org.codehaus.commons.compiler.CompileException;
import org.codehaus.janino.ExpressionEvaluator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.lang.reflect.InvocationTargetException;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Pattern;

public final class ExpressionUtils {

    private static final Pattern FORMULA_PATTERN = Pattern.compile("[\\d+\\-*/()\\s.]+");
    private static final Logger log = LoggerFactory.getLogger(ExpressionUtils.class);

    /**
     * Parses an expression as a double and returns the interpreted value of a property. Only mathematical expressions using
     * +, -, *, /, ( and ) are supported. In addition to basic arithmetic, the following variables are supported:
     *
     * <ul>
     *     <li><em>cpu</em> - the number of CPUs (cores) available to the system</li>
     * </ul>
     *
     * @param expression mathematical expression
     * @return the result of interpreting the mathematical expression, or {@link OptionalDouble#empty()} if
     *         {@code expression} is undefined or invalid.
     */
    @Nonnull
    public static OptionalDouble parseExpressionAsDouble(@Nullable String expression) {
        return parseExpression(expression, OptionalDouble::of, OptionalDouble::empty);
    }

    /**
     * Parses an expression as an int and returns the interpreted value of a property. Only mathematical expressions using
     * +, -, *, /, ( and ) are supported. In addition to basic arithmetic, the following variables are supported:
     *
     * <ul>
     *     <li><em>cpu</em> - the number of CPUs (cores) available to the system</li>
     * </ul>
     *
     * @param expression mathematical expression
     * @return the result of interpreting the mathematical expression, or {@link OptionalInt#empty()} if
     *         {@code expression} is undefined or invalid.
     */
    @Nonnull
    public static OptionalInt parseExpressionAsInt(@Nullable String expression) {
        OptionalLong longValue = parseExpressionAsLong(expression);
        if (longValue.isPresent()) {
            return OptionalInt.of(Ints.saturatedCast(longValue.getAsLong()));
        }
        return OptionalInt.empty();
    }

    /**
     * Parses an expression as a long and returns the interpreted value of a property. Only mathematical expressions using
     * +, -, *, /, ( and ) are supported. In addition to basic arithmetic, the following variables are supported:
     *
     * <ul>
     *     <li><em>cpu</em> - the number of CPUs (cores) available to the system</li>
     * </ul>
     *
     * @param expression mathematical expression
     * @return the result of interpreting the mathematical expression, or {@link OptionalLong#empty()} if
     *         {@code expression} is undefined or invalid
     */
    @Nonnull
    public static OptionalLong parseExpressionAsLong(@Nullable String expression) {
        return parseExpression(expression,
                value -> OptionalLong.of(Math.round(value)), OptionalLong::empty);
    }

    @Nonnull
    private static <OV> OV parseExpression(@Nullable String expression, Function<Double, OV> toOptional, Supplier<OV> emptyOptional) {
        if (expression != null) {
            expression = expression.toLowerCase().replace("cpu", Integer.toString(Runtime.getRuntime().availableProcessors()));
            if (FORMULA_PATTERN.matcher(expression).matches()) {
                try {
                    ExpressionEvaluator evaluator = new ExpressionEvaluator(expression, double.class, new String[] {}, new Class[] {});
                    return toOptional.apply((Double) evaluator.evaluate(new Object[] {}));
                } catch (CompileException e) {
                    log.debug("Error while parsing expression " + expression, e);
                } catch (InvocationTargetException e) {
                    log.debug("Error while evaluating expression " + expression, e);
                }
            }
        }
        return emptyOptional.get();
    }
}
