package p.studio.compiler.pbs.parser;

import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.pbs.lexer.PbsTokenKind;
import p.studio.compiler.source.Span;

/**
 * Dedicated expression parser for PBS precedence and orchestration.
 */
final class PbsExprParser {
    @FunctionalInterface
    interface BlockParserDelegate {
        PbsAst.Block parse(String message);
    }

    private final PbsExprParserContext context;
    private final PbsTokenCursor cursor;
    private final PbsExprControlFlowParser controlFlowParser;
    private final PbsExprPrimaryParser primaryParser;

    PbsExprParser(
            final PbsParserContext parserContext,
            final BlockParserDelegate blockParserDelegate) {
        this.context = new PbsExprParserContext(parserContext, blockParserDelegate);
        this.cursor = context.cursor();
        this.controlFlowParser = new PbsExprControlFlowParser(context, this::parseExpression, this::parsePrecedenceExpression);
        this.primaryParser = new PbsExprPrimaryParser(context, this::parseExpression, controlFlowParser::parseErrorPath);
    }

    PbsAst.Expression parseExpression() {
        return controlFlowParser.parseHandle();
    }

    private PbsAst.Expression parsePrecedenceExpression() {
        return parseApply();
    }

    private PbsAst.Expression parseApply() {
        final var left = parseOr();
        if (!cursor.match(PbsTokenKind.APPLY)) {
            return left;
        }

        final var right = parseApply();
        return new PbsAst.ApplyExpr(left, right, span(left.span().getStart(), right.span().getEnd()));
    }

    private PbsAst.Expression parseOr() {
        var expression = parseAnd();
        while (cursor.match(PbsTokenKind.OR_OR, PbsTokenKind.OR)) {
            final var operator = cursor.previous();
            final var right = parseAnd();
            expression = new PbsAst.BinaryExpr(
                    operator.lexeme(),
                    expression,
                    right,
                    span(expression.span().getStart(), right.span().getEnd()));
        }
        return expression;
    }

    private PbsAst.Expression parseAnd() {
        var expression = parseEquality();
        while (cursor.match(PbsTokenKind.AND_AND, PbsTokenKind.AND)) {
            final var operator = cursor.previous();
            final var right = parseEquality();
            expression = new PbsAst.BinaryExpr(
                    operator.lexeme(),
                    expression,
                    right,
                    span(expression.span().getStart(), right.span().getEnd()));
        }
        return expression;
    }

    private PbsAst.Expression parseEquality() {
        var expression = parseComparison();
        if (cursor.match(PbsTokenKind.EQUAL_EQUAL, PbsTokenKind.BANG_EQUAL)) {
            final var operator = cursor.previous();
            final var right = parseComparison();
            expression = new PbsAst.BinaryExpr(
                    operator.lexeme(),
                    expression,
                    right,
                    span(expression.span().getStart(), right.span().getEnd()));
            if (cursor.check(PbsTokenKind.EQUAL_EQUAL) || cursor.check(PbsTokenKind.BANG_EQUAL)) {
                context.report(cursor.peek(), ParseErrors.E_PARSE_NON_ASSOC, "Chained equality is not allowed");
                while (cursor.match(PbsTokenKind.EQUAL_EQUAL, PbsTokenKind.BANG_EQUAL)) {
                    parseComparison();
                }
            }
        }
        return expression;
    }

    private PbsAst.Expression parseComparison() {
        var expression = parseAs();
        if (cursor.match(PbsTokenKind.LESS, PbsTokenKind.LESS_EQUAL, PbsTokenKind.GREATER, PbsTokenKind.GREATER_EQUAL)) {
            final var operator = cursor.previous();
            final var right = parseAs();
            expression = new PbsAst.BinaryExpr(
                    operator.lexeme(),
                    expression,
                    right,
                    span(expression.span().getStart(), right.span().getEnd()));
            if (cursor.check(PbsTokenKind.LESS)
                    || cursor.check(PbsTokenKind.LESS_EQUAL)
                    || cursor.check(PbsTokenKind.GREATER)
                    || cursor.check(PbsTokenKind.GREATER_EQUAL)) {
                context.report(cursor.peek(), ParseErrors.E_PARSE_NON_ASSOC, "Chained comparison is not allowed");
                while (cursor.match(PbsTokenKind.LESS, PbsTokenKind.LESS_EQUAL, PbsTokenKind.GREATER, PbsTokenKind.GREATER_EQUAL)) {
                    parseAs();
                }
            }
        }
        return expression;
    }

    private PbsAst.Expression parseAs() {
        var expression = parseTerm();
        if (cursor.match(PbsTokenKind.AS)) {
            final var contract = context.consume(PbsTokenKind.IDENTIFIER, "Expected contract identifier after 'as'");
            expression = new PbsAst.AsExpr(
                    expression,
                    contract.lexeme(),
                    span(expression.span().getStart(), contract.end()));
        }
        return expression;
    }

    private PbsAst.Expression parseTerm() {
        var expression = parseFactor();
        while (cursor.match(PbsTokenKind.PLUS, PbsTokenKind.MINUS)) {
            final var operator = cursor.previous();
            final var right = parseFactor();
            expression = new PbsAst.BinaryExpr(
                    operator.lexeme(),
                    expression,
                    right,
                    span(expression.span().getStart(), right.span().getEnd()));
        }
        return expression;
    }

    private PbsAst.Expression parseFactor() {
        var expression = parseUnary();
        while (cursor.match(PbsTokenKind.STAR, PbsTokenKind.SLASH, PbsTokenKind.PERCENT)) {
            final var operator = cursor.previous();
            final var right = parseUnary();
            expression = new PbsAst.BinaryExpr(
                    operator.lexeme(),
                    expression,
                    right,
                    span(expression.span().getStart(), right.span().getEnd()));
        }
        return expression;
    }

    private PbsAst.Expression parseUnary() {
        if (cursor.match(PbsTokenKind.BANG, PbsTokenKind.MINUS, PbsTokenKind.NOT)) {
            final var operator = cursor.previous();
            final var right = parseUnary();
            return new PbsAst.UnaryExpr(operator.lexeme(), right, span(operator.start(), right.span().getEnd()));
        }
        return primaryParser.parsePostfix();
    }

    private Span span(final long start, final long end) {
        return context.span(start, end);
    }
}
