package expression.exceptions;

import expression.*;

/**
 * @author Doschennikov Nikita (me@fymio.us)
 */
public class ExpressionParser implements ListParser {

    private String src;
    private int pos;
    private java.util.List<String> variables;

    @Override
    public ListExpression parse(String expression, java.util.List<String> variables) {
        // System.err.println("PARSE: [" + expression + "]");
        this.src = expression;
        this.pos = 0;
        this.variables = variables;
        AbstractExpression result = parseMinMax();
        skipWhitespace();
        if (pos < src.length()) {
            throw new IllegalArgumentException(
                    "Unexpected character '" + src.charAt(pos) + "' at position " + pos);
        }
        return result;
    }

    private AbstractExpression parseMinMax() {
        AbstractExpression left = parseAddSub();
        while (true) {
            skipWhitespace();
            if      (tryConsume("min"))   left = new Min(left, parseAddSub());
            else if (tryConsume("max"))   left = new Max(left, parseAddSub());
            else if (tryConsume("set"))   left = new SetBit(left, parseAddSub());
            else if (tryConsume("clear")) left = new Clear(left, parseAddSub());
            else break;
        }
        return left;
    }

    private AbstractExpression parseAddSub() {
        AbstractExpression left = parseMulDiv();
        while (true) {
            skipWhitespace();
            if (pos < src.length() && src.charAt(pos) == '+') {
                pos++;
                left = new CheckedAdd(left, parseMulDiv());
            } else if (pos < src.length() && src.charAt(pos) == '-') {
                pos++;
                left = new CheckedSubtract(left, parseMulDiv());
            } else break;
        }
        return left;
    }

    private AbstractExpression parseMulDiv() {
        AbstractExpression left = parsePower();
        while (true) {
            skipWhitespace();
            if (pos < src.length() && src.charAt(pos) == '*' && nextCharIs('*')) {
                pos++;
                left = new CheckedMultiply(left, parsePower());
            } else if (pos < src.length() && src.charAt(pos) == '/' && nextCharIs('/')) {
                pos++;
                left = new CheckedDivide(left, parsePower());
            } else break;
        }
        return left;
    }

    private AbstractExpression parsePower() {
        AbstractExpression left = parseUnary();
        while (true) {
            skipWhitespace();
            if (pos + 1 < src.length() && src.charAt(pos) == '*' && src.charAt(pos + 1) == '*') {
                pos += 2;
                left = new Power(left, parseUnary());
            } else if (pos + 1 < src.length() && src.charAt(pos) == '/' && src.charAt(pos + 1) == '/') {
                pos += 2;
                left = new Log(left, parseUnary());
            } else break;
        }
        return left;
    }

    private AbstractExpression parseUnary() {
        skipWhitespace();
        if (pos >= src.length())
            throw new IllegalArgumentException("Unexpected end of expression at position " + pos);

        if (src.charAt(pos) == '-') {
            pos++;
            if (pos < src.length() && Character.isDigit(src.charAt(pos))) return parsePostfix(parseNumber(true));
            return new CheckedNegate(parseUnary());
        }

        // abs: ||x||  (U+2016 DOUBLE VERTICAL LINE)
        if (src.charAt(pos) == '‖') {
            pos++;
            AbstractExpression inner = parseMinMax();
            skipWhitespace();
            if (pos >= src.length() || src.charAt(pos) != '‖')
                throw new IllegalArgumentException("Expected ‖ at position " + pos);
            pos++;
            return parsePostfix(new Abs(inner));
        }

        // prefix unary: sqrt U+221A, cbrt U+221B
        if (src.charAt(pos) == '√') { pos++; return parsePostfix(new Sqrt(parseUnary())); }
        if (src.charAt(pos) == '∛') { pos++; return parsePostfix(new Cbrt(parseUnary())); }

        if (tryConsume("reverse")) return parsePostfix(new Reverse(parseUnary()));
        if (tryConsume("digits"))  return parsePostfix(new Digits(parseUnary()));
        if (tryConsume("floor"))   return parsePostfix(new Floor(parseUnary()));
        if (tryConsume("ceiling")) return parsePostfix(new Ceiling(parseUnary()));
        if (tryConsume("log₂") || tryConsume("log2")) return parsePostfix(new Log2(parseUnary()));
        if (tryConsume("pow₂") || tryConsume("pow2")) return parsePostfix(new Pow2(parseUnary()));
        if (tryConsume("low"))     return parsePostfix(new Low(parseUnary()));
        if (tryConsume("high"))    return parsePostfix(new High(parseUnary()));

        return parsePostfix(parsePrimary());
    }

    private AbstractExpression parsePostfix(AbstractExpression base) {
        skipWhitespace();
        if (pos < src.length() && src.charAt(pos) == '²') { pos++; return parsePostfix(new Square(base)); }
        if (pos < src.length() && src.charAt(pos) == '³') { pos++; return parsePostfix(new Cube(base)); }
        return base;
    }

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

        if (c == '(') {
            pos++;
            AbstractExpression inner = parseMinMax();
            skipWhitespace();
            expect();
            return inner;
        }
        if (c == '$') { pos++; return new Variable(parseIndex()); }
        if (Character.isDigit(c)) return parseNumber(false);

        if (Character.isLetter(c)) {
            int start = pos;
            while (pos < src.length() && (Character.isLetterOrDigit(src.charAt(pos)) || src.charAt(pos) == '_'))
                pos++;
            String name = src.substring(start, pos);
            int idx = variables != null ? variables.indexOf(name) : -1;
            if (idx >= 0) return new Variable(idx, name);
            throw new IllegalArgumentException("Unknown identifier '" + name + "' at position " + start);
        }

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

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

    private boolean nextCharIs(char next) {
        return pos + 1 >= src.length() || src.charAt(pos + 1) != next;
    }

    private boolean tryConsume(String keyword) {
        skipWhitespace();
        if (!src.startsWith(keyword, pos)) return false;
        int end = pos + keyword.length();
        if (end < src.length()) {
            char next = src.charAt(end);
            if (Character.isLetterOrDigit(next) || next == '_') return false;
        }
        if (variables != null && variables.contains(keyword)) return false;
        pos = end;
        return true;
    }

    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++;
    }

    private AbstractExpression 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);
        int result = 0;
        for (int i = 0; i < numStr.length(); i++) {
            int digit = numStr.charAt(i) - '0';
            if (!negative) {
                if (result > (Integer.MAX_VALUE - digit) / 10)
                    throw new OverflowException("constant " + numStr);
                result = result * 10 + digit;
            } else {
                if (result < (Integer.MIN_VALUE + digit) / 10)
                    throw new OverflowException("constant -" + numStr);
                result = result * 10 - digit;
            }
        }
        return new Const(result);
    }

    private int parseIndex() {
        int start = pos;
        while (pos < src.length() && Character.isDigit(src.charAt(pos))) pos++;
        if (start == pos) throw new IllegalArgumentException("Expected digit after '$' at position " + pos);
        return Integer.parseInt(src.substring(start, pos));
    }
}