package ru.yandex.webmaster3.monitoring.solomon.trigger.expressions;

import com.google.common.base.Preconditions;
import org.joda.time.Duration;
import ru.yandex.webmaster3.core.util.joda.DurationSerializationUtil;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @author avhaliullin
 */
public class SolomonExpressionsBuilder {

    // Boolean

    public static <T> SolomonTriggerExpression<T> ternary(SolomonTriggerExpression<Boolean> condition, SolomonTriggerExpression<T> trueOpt, SolomonTriggerExpression<T> falseOpt) {
        return new TernaryOperator<>(condition, trueOpt, falseOpt);
    }

    public static SolomonTriggerExpression<Boolean> or(Collection<SolomonTriggerExpression<Boolean>> args) {
        return new AssociativeOperator<>("||", args);
    }

    @SafeVarargs
    public static SolomonTriggerExpression<Boolean> or(SolomonTriggerExpression<Boolean> arg0, SolomonTriggerExpression<Boolean>... args) {
        return associative("||", arg0, args);
    }

    public static SolomonTriggerExpression<Boolean> and(Collection<SolomonTriggerExpression<Boolean>> args) {
        return new AssociativeOperator<>("&&", args);
    }

    @SafeVarargs
    public static SolomonTriggerExpression<Boolean> and(SolomonTriggerExpression<Boolean> arg0, SolomonTriggerExpression<Boolean>... args) {
        return associative("&&", arg0, args);
    }

    public static SolomonTriggerExpression<Boolean> not(SolomonTriggerExpression<Boolean> arg) {
        return new UnaryOperator<>("!", arg);
    }

    // Comparison

    public static SolomonTriggerExpression<Boolean> lt(SolomonTriggerExpression<Number> leftArg, SolomonTriggerExpression<Number> rightArg) {
        return new BinaryOperator<>("<", leftArg, rightArg);
    }

    public static SolomonTriggerExpression<Boolean> lte(SolomonTriggerExpression<Number> leftArg, SolomonTriggerExpression<Number> rightArg) {
        return new BinaryOperator<>("<=", leftArg, rightArg);
    }

    public static SolomonTriggerExpression<Boolean> gt(SolomonTriggerExpression<Number> leftArg, SolomonTriggerExpression<Number> rightArg) {
        return new BinaryOperator<>(">", leftArg, rightArg);
    }

    public static SolomonTriggerExpression<Boolean> gte(SolomonTriggerExpression<Number> leftArg, SolomonTriggerExpression<Number> rightArg) {
        return new BinaryOperator<>(">=", leftArg, rightArg);
    }

    public static SolomonTriggerExpression<Boolean> eq(SolomonTriggerExpression<Number> leftArg, SolomonTriggerExpression<Number> rightArg) {
        return new BinaryOperator<>("==", leftArg, rightArg);
    }

    public static SolomonTriggerExpression<Boolean> ne(SolomonTriggerExpression<Number> leftArg, SolomonTriggerExpression<Number> rightArg) {
        return new BinaryOperator<>("!=", leftArg, rightArg);
    }

    // Arithmetics

    public static SolomonTriggerExpression<Number> sum(Collection<SolomonTriggerExpression<Number>> args) {
        return new AssociativeOperator<>("+", args);
    }

    @SafeVarargs
    public static SolomonTriggerExpression<Number> sum(SolomonTriggerExpression<Number> arg0, SolomonTriggerExpression<Number>... args) {
        return associative("+", arg0, args);
    }

    public static SolomonTriggerExpression<Number> mul(Collection<SolomonTriggerExpression<Number>> args) {
        return new AssociativeOperator<>("*", args);
    }

    @SafeVarargs
    public static SolomonTriggerExpression<Number> mul(SolomonTriggerExpression<Number> arg0, SolomonTriggerExpression<Number>... args) {
        return associative("*", arg0, args);
    }

    public static SolomonTriggerExpression<Number> sub(SolomonTriggerExpression<Number> leftArg, SolomonTriggerExpression<Number> rightArg) {
        return new BinaryOperator<>("-", leftArg, rightArg);
    }

    public static SolomonTriggerExpression<Number> div(SolomonTriggerExpression<Number> leftArg, SolomonTriggerExpression<Number> rightArg) {
        return new BinaryOperator<>("/", leftArg, rightArg);
    }

    public static SolomonTriggerExpression<Number> abs(SolomonTriggerExpression<Number> arg) {
        return new Func<>("abs", arg);
    }

    public static SolomonTriggerExpression<Number> sqrt(SolomonTriggerExpression<Number> arg) {
        return new Func<>("sqrt", arg);
    }

    public static SolomonTriggerExpression<Number> sqr(SolomonTriggerExpression<Number> arg) {
        return new Func<>("sqr", arg);
    }

    public static SolomonTriggerExpression<Number> toFixed(SolomonTriggerExpression<Number> arg, SolomonTriggerExpression<Number> digits) {
        return new Func<>("to_fixed", arg, digits);
    }

    public static SolomonTriggerExpression<Number> random01() {
        return new Func<>("random01");
    }

    // Aggregation

    public static SolomonTriggerExpression<Number> min(SolomonTriggerExpression<SolomonGraphData> data) {
        return new Func<Number>("min", data);
    }

    public static SolomonTriggerExpression<Number> max(SolomonTriggerExpression<SolomonGraphData> data) {
        return new Func<Number>("max", data);
    }

    public static SolomonTriggerExpression<Number> last(SolomonTriggerExpression<SolomonGraphData> data) {
        return new Func<Number>("last", data);
    }

    public static SolomonTriggerExpression<Number> avg(SolomonTriggerExpression<SolomonGraphData> data) {
        return new Func<Number>("avg", data);
    }

    public static SolomonTriggerExpression<Number> sum(SolomonTriggerExpression<SolomonGraphData> data) {
        return new Func<Number>("sum", data);
    }

    public static SolomonTriggerExpression<Number> integrate(SolomonTriggerExpression<SolomonGraphData> data) {
        return new Func<Number>("integrate", data);
    }

    public static SolomonTriggerExpression<Number> count(SolomonTriggerExpression<SolomonGraphData> data) {
        return new Func<Number>("count", data);
    }

    public static SolomonTriggerExpression<Number> std(SolomonTriggerExpression<SolomonGraphData> data) {
        return new Func<Number>("std", data);
    }

    public static SolomonTriggerExpression<SolomonGraphData> shift(SolomonTriggerExpression<SolomonGraphData> data,
                                                                   SolomonTriggerExpression<Duration> duration) {
        return new Func<SolomonGraphData>("shift", data, duration);
    }

    public static SolomonTriggerExpression<SolomonGraphData> head(SolomonTriggerExpression<SolomonGraphData> data,
                                                                   SolomonTriggerExpression<Duration> duration) {
        return new Func<SolomonGraphData>("head", data, duration);
    }

    public static SolomonTriggerExpression<SolomonGraphData> tail(SolomonTriggerExpression<SolomonGraphData> data,
                                                                   SolomonTriggerExpression<Duration> duration) {
        return new Func<SolomonGraphData>("tail", data, duration);
    }

    // Terms

    public static SolomonTriggerExpression<Boolean> TRUE = new SimpleExpression<>("true");
    public static SolomonTriggerExpression<Boolean> FALSE = new SimpleExpression<>("false");

    public static SolomonTriggerExpression<Boolean> bool(boolean value) {
        return value ? TRUE : FALSE;
    }

    public static SolomonTriggerExpression<Number> num(Number num) {
        return new SimpleExpression<>(num.toString());
    }

    public static SolomonTriggerExpression<Number> numVar(String name) {
        return new SimpleExpression<>(name);
    }

    public static SolomonTriggerExpression<Boolean> boolVar(String name) {
        return new SimpleExpression<>(name);
    }

    public static SolomonTriggerExpression<SolomonGraphData> variable(String name) {
        return new SimpleExpression<>(name);
    }

    public static SolomonTriggerExpression<String> string(String value) {
        return new StringValue(value);
    }

    public static SolomonTriggerExpression<Duration> duration(Duration value) {
        return new SimpleExpression<>(DurationSerializationUtil.serializeDuration(value));
    }

    // Hack

    public static <T> SolomonTriggerExpression<T> customProgram(String program) {
        return new CustomProgram<>(program);
    }

    // Internal classes

    static class SimpleExpression<T> extends SolomonTriggerExpression<T> {
        private final String expression;

        SimpleExpression(String expression) {
            this.expression = expression;
        }

        @Override
        public String renderExpression() {
            return expression;
        }
    }

    static class BinaryOperator<S, T> extends SolomonTriggerExpression<T> {
        private final String operator;
        private final SolomonTriggerExpression<S> leftArg;
        private final SolomonTriggerExpression<S> rightArg;

        public BinaryOperator(String operator, SolomonTriggerExpression<S> leftArg, SolomonTriggerExpression<S> rightArg) {
            this.leftArg = leftArg;
            this.rightArg = rightArg;
            this.operator = operator;
        }

        @Override
        public String renderExpression() {
            return "(" + leftArg.renderExpression() + " " + operator + " " + rightArg.renderExpression() + ")";
        }
    }

    private static <T> AssociativeOperator<T> associative(String operator, SolomonTriggerExpression<T> arg0, SolomonTriggerExpression<T>... args) {
        List<SolomonTriggerExpression<T>> argsList = new ArrayList<>();
        argsList.add(arg0);
        argsList.addAll(Arrays.asList(args));
        return new AssociativeOperator<>(operator, argsList);
    }

    static class AssociativeOperator<T> extends SolomonTriggerExpression<T> {
        private final String operator;
        private final Collection<SolomonTriggerExpression<T>> children;

        public AssociativeOperator(String operator, Collection<SolomonTriggerExpression<T>> children) {
            Preconditions.checkState(!children.isEmpty(), "No arguments passed for operator " + operator);
            this.operator = operator;
            this.children = children;
        }

        @Override
        public String renderExpression() {
            return "(" +
                    children.stream()
                            .map(SolomonTriggerExpression::renderExpression)
                            .collect(Collectors.joining(" " + operator + " ")) +
                    ")";
        }
    }

    static class TernaryOperator<T> extends SolomonTriggerExpression<T> {
        private final SolomonTriggerExpression<Boolean> condition;
        private final SolomonTriggerExpression<T> trueOpt;
        private final SolomonTriggerExpression<T> falseOpt;

        public TernaryOperator(SolomonTriggerExpression<Boolean> condition, SolomonTriggerExpression<T> trueOpt, SolomonTriggerExpression<T> falseOpt) {
            this.condition = condition;
            this.trueOpt = trueOpt;
            this.falseOpt = falseOpt;
        }

        @Override
        public String renderExpression() {
            return "(" + condition.renderExpression() + " ? " + trueOpt.renderExpression() + " : " + falseOpt.renderExpression() + ")";
        }
    }

    static class UnaryOperator<T> extends SolomonTriggerExpression<T> {
        private final String operator;
        private final SolomonTriggerExpression<T> arg;

        public UnaryOperator(String operator, SolomonTriggerExpression<T> arg) {
            this.operator = operator;
            this.arg = arg;
        }

        @Override
        public String renderExpression() {
            return operator + arg.renderExpression();
        }
    }

    static class Func<R> extends SolomonTriggerExpression<R> {
        private final String name;
        private final SolomonTriggerExpression[] args;

        public Func(String name, SolomonTriggerExpression... args) {
            this.name = name;
            this.args = args;
        }

        @Override
        public String renderExpression() {
            return name + "(" + Arrays.stream(args).map(SolomonTriggerExpression::renderExpression).collect(Collectors.joining(", ")) + ")";
        }
    }

    static class StringValue extends SolomonTriggerExpression<String> {

        private final String value;

        StringValue(String value) {
            this.value = value;
        }

        @Override
        public String renderExpression() {
            return "'" + value + "'";
        }
    }

    static class CustomProgram<T> extends SolomonTriggerExpression<T> {
        private final String program;

        public CustomProgram(String program) {
            this.program = program;
        }

        @Override
        public String renderExpression() {
            return program;
        }
    }
}
