package expression.generic;

import java.util.List;

/**
 * @author Doschennikov Nikita (me@fymio.us)
 */
public class GenericParser {

    private static final List<String> VARIABLES = List.of("x", "y", "z");

    private String src;
    private int pos;

    public <T> GenericExpr<T> parse(String expression) {
        this.src = expression;
        this.pos = 0;
        GenericExpr<T> result = parseMinMax();
        skipWhitespace();
        if (pos < src.length()) {
            throw new IllegalArgumentException(
                "Unexpected character '" +
                    src.charAt(pos) +
                    "' at position " +
                    pos
            );
        }
        return result;
    }

    private <T> GenericExpr<T> parseMinMax() {
        GenericExpr<T> left = parseAddSub();
        while (true) {
            skipWhitespace();
            if (matchKeyword("min")) {
                GenericExpr<T> right = parseAddSub();
                GenericExpr<T> l = left;
                left = (type, vars) ->
                    type.min(
                        l.evaluate(type, vars),
                        right.evaluate(type, vars)
                    );
            } else if (matchKeyword("max")) {
                GenericExpr<T> right = parseAddSub();
                GenericExpr<T> l = left;
                left = (type, vars) ->
                    type.max(
                        l.evaluate(type, vars),
                        right.evaluate(type, vars)
                    );
            } else {
                break;
            }
        }
        return left;
    }

    private <T> GenericExpr<T> parseAddSub() {
        GenericExpr<T> left = parseMulDiv();
        while (true) {
            skipWhitespace();
            if (pos < src.length() && src.charAt(pos) == '+') {
                pos++;
                GenericExpr<T> right = parseMulDiv();
                GenericExpr<T> l = left;
                left = (type, vars) ->
                    type.add(
                        l.evaluate(type, vars),
                        right.evaluate(type, vars)
                    );
            } else if (
                pos < src.length() && src.charAt(pos) == '-' && !nextIsKeyword()
            ) {
                pos++;
                GenericExpr<T> right = parseMulDiv();
                GenericExpr<T> l = left;
                left = (type, vars) ->
                    type.subtract(
                        l.evaluate(type, vars),
                        right.evaluate(type, vars)
                    );
            } else {
                break;
            }
        }
        return left;
    }

    private <T> GenericExpr<T> parseMulDiv() {
        GenericExpr<T> left = parseUnary();
        while (true) {
            skipWhitespace();
            if (pos < src.length() && src.charAt(pos) == '*') {
                pos++;
                GenericExpr<T> right = parseUnary();
                GenericExpr<T> l = left;
                left = (type, vars) ->
                    type.multiply(
                        l.evaluate(type, vars),
                        right.evaluate(type, vars)
                    );
            } else if (pos < src.length() && src.charAt(pos) == '/') {
                pos++;
                GenericExpr<T> right = parseUnary();
                GenericExpr<T> l = left;
                left = (type, vars) ->
                    type.divide(
                        l.evaluate(type, vars),
                        right.evaluate(type, vars)
                    );
            } else {
                break;
            }
        }
        return left;
    }

    private <T> GenericExpr<T> parseUnary() {
        skipWhitespace();
        if (pos >= src.length()) {
            throw new IllegalArgumentException(
                "Unexpected end of expression at position " + pos
            );
        }
        if (src.charAt(pos) == '-') {
            pos++;
            skipWhitespace();
            if (pos < src.length() && Character.isDigit(src.charAt(pos))) {
                return parseNumber(true);
            }
            GenericExpr<T> operand = parseUnary();
            return (type, vars) -> type.negate(operand.evaluate(type, vars));
        }
        if (matchKeyword("count")) {
            GenericExpr<T> operand = parseUnary();
            return (type, vars) -> type.count(operand.evaluate(type, vars));
        }
        return parsePrimary();
    }

    private <T> GenericExpr<T> parsePrimary() {
        skipWhitespace();
        if (pos >= src.length()) {
            throw new IllegalArgumentException("Unexpected end of expression");
        }
        char c = src.charAt(pos);

        if (c == '(') {
            pos++;
            GenericExpr<T> inner = parseMinMax();
            skipWhitespace();
            expect();
            return inner;
        }

        if (Character.isDigit(c)) {
            return parseNumber(false);
        }

        if (Character.isLetter(c)) {
            int start = pos;
            while (
                pos < src.length() && Character.isLetterOrDigit(src.charAt(pos))
            ) {
                pos++;
            }
            String name = src.substring(start, pos);
            int idx = VARIABLES.indexOf(name);
            if (idx >= 0) {
                return (type, vars) -> vars.get(idx);
            }
            throw new IllegalArgumentException(
                "Unknown variable '" + name + "' at position " + start
            );
        }

        throw new IllegalArgumentException(
            "Unexpected character '" + c + "' at position " + pos
        );
    }

    private <T> GenericExpr<T> parseNumber(boolean negative) {
        int start = pos;
        while (pos < src.length() && Character.isDigit(src.charAt(pos))) {
            pos++;
        }
        if (start == pos) {
            throw new IllegalArgumentException(
                "Expected digit at position " + pos
            );
        }
        String numStr = src.substring(start, pos);
        long val = Long.parseLong(numStr);
        if (negative) val = -val;
        if (val < Integer.MIN_VALUE || val > Integer.MAX_VALUE) {
            throw new IllegalArgumentException("Integer overflow: " + val);
        }
        int intVal = (int) val;
        return (type, vars) -> type.fromInt(intVal);
    }

    private boolean matchKeyword(String keyword) {
        int i = pos;
        for (char ch : keyword.toCharArray()) {
            if (i >= src.length() || src.charAt(i) != ch) return false;
            i++;
        }
        if (
            i < src.length() && Character.isLetterOrDigit(src.charAt(i))
        ) return false;
        pos = i;
        return true;
    }

    private boolean nextIsKeyword() {
        return false;
    }

    private void skipWhitespace() {
        while (pos < src.length() && Character.isWhitespace(src.charAt(pos))) {
            pos++;
        }
    }

    private void expect() {
        if (pos >= src.length() || src.charAt(pos) != ')') {
            throw new IllegalArgumentException(
                "Expected '" +
                        ')' +
                    "' at position " +
                    pos +
                    (pos < src.length()
                        ? ", got '" + src.charAt(pos) + "'"
                        : ", got end of input")
            );
        }
        pos++;
    }
}
