module api_modules.classroom_compliance.score_eval;

import std.regex;
import std.uni;
import std.string;
import std.range;
import std.array;
import std.conv;

Expr parseExpression(string expr) {
    Token[] tokens = lexer(expr);
    return parseExpr(tokens);
}

double evaluateExpression(string expr, double[string] ctx = (double[string]).init) {
    Expr ast = parseExpression(expr);
    return ast.eval(ctx);
}

unittest {
    assert(evaluateExpression("3 + 4") == 7.0);
    assert(evaluateExpression("3 + 4 + 1") == 8.0);
    assert(evaluateExpression("3 * 4 + 1") == 13.0);
    assert(evaluateExpression("3 * (4 + 1)") == 15.0);
    assert(evaluateExpression("(1 + 2) * (3 + 5)") == 24.0);
    Expr e1 = parseExpression("x + 1");
    assert(e1.eval(["x": 1.0]) == 2.0);
    assert(e1.eval(["x": 3.0]) == 4.0);
    Expr e2 = parseExpression("0.3 * phone + 0.7 * (behavior_good * 1 + behavior_mediocre * 0.5)");
    // Test a perfect score first.
    assert(e2.eval([
        "phone": 1.0,
        "behavior_good": 1.0,
        "behavior_mediocre": 0.0
    ]) == 1.0);
    // Test 0.5 phone score.
    assert(e2.eval([
        "phone": 0.5,
        "behavior_good": 1.0,
        "behavior_mediocre": 0.0
    ]) == 0.85);
    // Test 0.75 phone, 0.5 good behavior, 0.5 mediocre behavior.
    double d = e2.eval([
        "phone": 0.75,
        "behavior_good": 0.5,
        "behavior_mediocre": 0.5
    ]);
    assert(d > 0.7499 && d < 0.7501);
}

bool validateExpression(string expr) {
    try {
        auto _ = parseExpression(expr);
        return true;
    } catch (ExpressionParsingException e) {
        return false;
    }
}

class ExpressionParsingException : Exception {
    this(string msg) {
        super(msg);
    }
}

class UnexpectedTokenException : ExpressionParsingException {
    Token token;
    this(Token token) {
        super("Unexpected token found while parsing expression: " ~ token.toString());
        this.token = token;
    }
}

class ExpressionEvaluationException : Exception {
    this(string msg) {
        super(msg);
    }
}

class UndefinedVariableException : ExpressionEvaluationException {
    string variableName;
    this(string variableName) {
        super("No value is defined for the variable \"" ~ variableName ~ "\".");
        this.variableName = variableName;
    }
}

private enum TokenType {
    ADD,
    SUB,
    MUL,
    DIV,
    PAREN_OPEN,
    PAREN_CLOSE,
    NUMBER,
    VARIABLE
}

private interface Token {
    TokenType getType() const;
    string toString() const;
}

private class SimpleToken : Token {
    const TokenType type;
    this(TokenType type) {
        this.type = type;
    }
    override TokenType getType() const {
        return type;
    }
    override string toString() const {
        return type.to!string;
    }
}

private class NumberToken : Token {
    const double value;
    this(double value) {
        this.value = value;
    }
    override TokenType getType() const {
        return TokenType.NUMBER;
    }
    override string toString() const {
        return value.to!string;
    }
}

private class VariableToken : Token {
    const string name;
    this(string name) {
        this.name = name;
    }
    override TokenType getType() const {
        return TokenType.VARIABLE;
    }
    override string toString() const {
        return name;
    }
}

private Token[] lexer(string expr) {
    auto app = appender!(Token[]);
    size_t idx = 0;
    while (idx < expr.length) {
        const char c = expr[idx];
        if (isWhite(c)) {
            idx++;
            continue;
        }
        if (isNumber(c)) {
            app ~= lexNumber(expr, idx);
        } else if (isAlpha(c)) {
            app ~= lexVariable(expr, idx);
        } else {
            switch (c) {
                case '+':
                    app ~= new SimpleToken(TokenType.ADD);
                    break;
                case '-':
                    app ~= new SimpleToken(TokenType.SUB);
                    break;
                case '*':
                    app ~= new SimpleToken(TokenType.MUL);
                    break;
                case '/':
                    app ~= new SimpleToken(TokenType.DIV);
                    break;
                case '(':
                    app ~= new SimpleToken(TokenType.PAREN_OPEN);
                    break;
                case ')':
                    app ~= new SimpleToken(TokenType.PAREN_CLOSE);
                    break;
                default:
                    throw new Exception("Invalid token: " ~ expr);
            }
            idx++;
        }
    }
    return app[];
}

unittest {
    Token[] t1 = lexer("3 + 4");
    assert(t1.length == 3);
    assert(t1[0].getType() == TokenType.NUMBER);
    assert((cast(NumberToken) t1[0]).value == 3.0);
    assert(t1[1].getType() == TokenType.ADD);
    assert(t1[2].getType() == TokenType.NUMBER);
    assert((cast(NumberToken) t1[2]).value == 4.0);
}

private NumberToken lexNumber(string s, ref size_t idx) {
    const r = regex("\\d+(\\.\\d+)?");
    auto captures = matchFirst(s[idx .. $], r);
    if (captures.empty) {
        throw new ExpressionParsingException("Failed to read number from string: " ~ s[idx .. $]);
    }
    idx += captures.front.length;
    return new NumberToken(captures.front.to!double);
}

private VariableToken lexVariable(string s, ref size_t idx) {
    const r = regex("[a-zA-Z_]+");
    auto captures = matchFirst(s[idx .. $], r);
    if (captures.empty) {
        throw new ExpressionParsingException("Failed to read variable from string: " ~ s[idx .. $]);
    }
    idx += captures.front.length;
    return new VariableToken(captures.front);
}

interface Expr {
    double eval(double[string] context) const;
    string toString() const;
}

private class BinaryOpExpr : Expr {
    private TokenType op;
    private Expr left;
    private Expr right;

    this(TokenType op, Expr left, Expr right) {
        this.op = op;
        this.left = left;
        this.right = right;
    }

    double eval(double[string] context) const {
        const l = left.eval(context);
        const r = right.eval(context);
        switch (op) {
            case TokenType.ADD:
                return l + r;
            case TokenType.SUB:
                return l - r;
            case TokenType.MUL:
                return l * r;
            case TokenType.DIV:
                return l / r;
            default:
                throw new ExpressionEvaluationException("Invalid binary operation: " ~ op.to!string);
        }
    }

    override string toString() const {
        return "(" ~ left.toString() ~ " " ~ this.op.to!string ~ " " ~ right.toString() ~ ")";
    }
}

private class NumberLiteralExpr : Expr {
    private double value;

    this(double value) {
        this.value = value;
    }

    double eval(double[string] context) const {
        return value;
    }

    override string toString() const {
        return value.to!string;
    }
}

private class VariableExpr : Expr {
    private string variable;

    this(string variable) {
        this.variable = variable;
    }

    double eval(double[string] context) const {
        if (variable !in context) throw new UndefinedVariableException(variable);
        return context[variable];
    }

    override string toString() const {
        return variable;
    }
}


private Expr parseExpr(ref Token[] tokens) {
    Expr left = parseTerm(tokens);
    if (empty(tokens)) return left;
    Token t = front(tokens);
    if (t.getType() == TokenType.ADD) {
        popFront(tokens);
        Expr right = parseExpr(tokens);
        return new BinaryOpExpr(TokenType.ADD, left, right);
    } else if (t.getType() == TokenType.SUB) {
        popFront(tokens);
        Expr right = parseExpr(tokens);
        return new BinaryOpExpr(TokenType.SUB, left, right);
    } else {
        throw new UnexpectedTokenException(t);
    }
}

private Expr parseTerm(ref Token[] tokens) {
    Expr left = parseFactor(tokens);
    if (empty(tokens)) return left;
    Token t = front(tokens);
    if (t.getType() == TokenType.MUL) {
        popFront(tokens);
        Expr right = parseFactor(tokens);
        return new BinaryOpExpr(TokenType.MUL, left, right);
    } else if (t.getType() == TokenType.DIV) {
        popFront(tokens);
        Expr right = parseFactor(tokens);
        return new BinaryOpExpr(TokenType.DIV, left, right);
    } else {
        return left;
    }
}

private Expr parseFactor(ref Token[] tokens) {
    Token t = front(tokens);
    if (t.getType() == TokenType.VARIABLE) {
        VariableToken vt = cast(VariableToken) t;
        popFront(tokens);
        return new VariableExpr(vt.name);
    } else if (t.getType() == TokenType.NUMBER) {
        NumberToken nt = cast(NumberToken) t;
        popFront(tokens);
        return new NumberLiteralExpr(nt.value);
    } else if (t.getType() == TokenType.PAREN_OPEN) {
        int i = 1;
        int pCount = 1;
        while (i < tokens.length && pCount > 0) {
            if (tokens[i].getType() == TokenType.PAREN_OPEN) pCount++;
            if (tokens[i].getType() == TokenType.PAREN_CLOSE) pCount--;
            i++;
        }
        if (pCount != 0) throw new ExpressionParsingException("Unbalanced parentheses.");
        Token[] subExprTokens = tokens[1 .. i - 1];
        for (size_t idx = 0; idx < i; idx++) popFront(tokens);
        return parseExpr(subExprTokens);
    } else {
        throw new UnexpectedTokenException(t);
    }
}
