package p.studio.compiler.pbs.parser;

import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.pbs.lexer.PbsToken;
import p.studio.compiler.pbs.lexer.PbsTokenKind;
import p.studio.utilities.structures.ReadOnlyList;

import java.util.ArrayList;

final class PbsExprPrimaryParser {
    private static final int MAX_TUPLE_LITERAL_ARITY = 6;

    @FunctionalInterface
    interface ExpressionParserDelegate {
        PbsAst.Expression parse();
    }

    @FunctionalInterface
    interface ErrorPathParserDelegate {
        PbsAst.ErrorPath parse();
    }

    private final PbsExprParserContext context;
    private final PbsTokenCursor cursor;
    private final ExpressionParserDelegate expressionParserDelegate;
    private final ErrorPathParserDelegate errorPathParserDelegate;
    private final PbsExprLiteralSupport literalSupport;

    PbsExprPrimaryParser(
            final PbsExprParserContext context,
            final ExpressionParserDelegate expressionParserDelegate,
            final ErrorPathParserDelegate errorPathParserDelegate) {
        this.context = context;
        this.cursor = context.cursor();
        this.expressionParserDelegate = expressionParserDelegate;
        this.errorPathParserDelegate = errorPathParserDelegate;
        this.literalSupport = new PbsExprLiteralSupport(context);
    }

    PbsAst.Expression parsePostfix() {
        var expression = parsePrimary();

        while (true) {
            if (cursor.match(PbsTokenKind.LEFT_PAREN)) {
                final var arguments = new ArrayList<PbsAst.Expression>();
                if (!cursor.check(PbsTokenKind.RIGHT_PAREN)) {
                    do {
                        arguments.add(expressionParserDelegate.parse());
                    } while (cursor.match(PbsTokenKind.COMMA));
                }
                final var close = context.consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after call arguments");
                expression = new PbsAst.CallExpr(expression, ReadOnlyList.wrap(arguments),
                        context.span(expression.span().getStart(), close.end()));
                continue;
            }

            if (cursor.match(PbsTokenKind.DOT)) {
                final var member = context.consumeMemberName("Expected member name after '.'");
                expression = new PbsAst.MemberExpr(
                        expression,
                        member.lexeme(),
                        context.span(expression.span().getStart(), member.end()));
                continue;
            }

            if (cursor.match(PbsTokenKind.BANG)) {
                final var bang = cursor.previous();
                expression = new PbsAst.PropagateExpr(expression, context.span(expression.span().getStart(), bang.end()));
                continue;
            }

            if (cursor.match(PbsTokenKind.QUESTION)) {
                final var question = cursor.previous();
                context.report(question, ParseErrors.E_PARSE_INVALID_PROPAGATE_OPERATOR,
                        "Use '!' as propagation operator; '?' is not valid PBS syntax");
                continue;
            }

            return expression;
        }
    }

    PbsAst.Expression parsePrimary() {
        final var literal = literalSupport.parseLiteralExpression();
        if (literal != null) {
            return literal;
        }
        if (cursor.match(PbsTokenKind.THIS)) {
            final var token = cursor.previous();
            return new PbsAst.ThisExpr(context.span(token.start(), token.end()));
        }
        if (cursor.match(PbsTokenKind.NEW)) {
            return parseNewExpression(cursor.previous());
        }
        if (cursor.match(PbsTokenKind.BIND)) {
            return parseBindExpression(cursor.previous());
        }
        if (cursor.match(PbsTokenKind.SOME)) {
            return parseSomeExpression(cursor.previous());
        }
        if (cursor.match(PbsTokenKind.NONE)) {
            final var token = cursor.previous();
            return new PbsAst.NoneExpr(context.span(token.start(), token.end()));
        }
        if (cursor.match(PbsTokenKind.OK)) {
            return parseOkExpression(cursor.previous());
        }
        if (cursor.match(PbsTokenKind.ERR)) {
            return parseErrExpression(cursor.previous());
        }
        if (cursor.match(PbsTokenKind.IDENTIFIER)) {
            final var token = cursor.previous();
            return new PbsAst.IdentifierExpr(token.lexeme(), context.span(token.start(), token.end()));
        }
        if (cursor.match(PbsTokenKind.LEFT_PAREN)) {
            return parseParenthesizedPrimary(cursor.previous());
        }
        if (cursor.check(PbsTokenKind.LEFT_BRACE)) {
            final var block = context.parseSurfaceBlock("Expected block expression");
            return new PbsAst.BlockExpr(block, block.span());
        }

        final var token = cursor.peek();
        context.report(token, ParseErrors.E_PARSE_UNEXPECTED_TOKEN, "Unexpected token in expression: " + token.kind());
        cursor.advance();
        return new PbsAst.IntLiteralExpr(0L, context.span(token.start(), token.end()));
    }

    PbsAst.Expression parseParenthesizedPrimary(final PbsToken open) {
        if (cursor.match(PbsTokenKind.RIGHT_PAREN)) {
            final var close = cursor.previous();
            return new PbsAst.UnitExpr(context.span(open.start(), close.end()));
        }

        if (cursor.check(PbsTokenKind.IDENTIFIER) && cursor.checkNext(PbsTokenKind.COLON)) {
            final var items = parseTupleItems(true);
            final var close = context.consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after tuple literal");
            if (items.size() < 2) {
                context.report(close, ParseErrors.E_PARSE_INVALID_TUPLE_LITERAL,
                        "Single-slot tuple literal is not allowed in PBS core syntax");
            }
            if (items.size() > MAX_TUPLE_LITERAL_ARITY) {
                context.report(close, ParseErrors.E_PARSE_INVALID_TUPLE_LITERAL,
                        "Tuple literal arity must be between 2 and 6 items");
            }
            return new PbsAst.TupleExpr(ReadOnlyList.wrap(items), context.span(open.start(), close.end()));
        }

        final var first = expressionParserDelegate.parse();
        if (!cursor.match(PbsTokenKind.COMMA)) {
            final var close = context.consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after grouped expression");
            return new PbsAst.GroupExpr(first, context.span(open.start(), close.end()));
        }

        final var items = new ArrayList<PbsAst.TupleItem>();
        items.add(new PbsAst.TupleItem(null, first, first.span()));
        var hasLabels = false;
        while (!cursor.check(PbsTokenKind.RIGHT_PAREN) && !cursor.isAtEnd()) {
            final var item = parseTupleItem();
            if (item.label() != null) {
                hasLabels = true;
            }
            items.add(item);
            if (!cursor.match(PbsTokenKind.COMMA)) {
                break;
            }
        }

        if (items.size() < 2) {
            context.report(cursor.peek(), ParseErrors.E_PARSE_INVALID_TUPLE_LITERAL,
                    "Tuple literals require at least two items");
        }

        final var close = context.consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after tuple literal");
        if (items.size() > MAX_TUPLE_LITERAL_ARITY) {
            context.report(close, ParseErrors.E_PARSE_INVALID_TUPLE_LITERAL,
                    "Tuple literal arity must be between 2 and 6 items");
        }
        if (hasLabels) {
            for (final var item : items) {
                if (item.label() == null) {
                    context.report(close, ParseErrors.E_PARSE_INVALID_TUPLE_LITERAL,
                            "Mixed labeled/unlabeled tuple items are not allowed");
                    break;
                }
            }
        }

        return new PbsAst.TupleExpr(ReadOnlyList.wrap(items), context.span(open.start(), close.end()));
    }

    private ArrayList<PbsAst.TupleItem> parseTupleItems(final boolean labeledOnly) {
        final var items = new ArrayList<PbsAst.TupleItem>();
        do {
            final var item = parseTupleItem();
            items.add(item);
            if (labeledOnly && item.label() == null) {
                context.report(cursor.previous(), ParseErrors.E_PARSE_INVALID_TUPLE_LITERAL,
                        "Named tuple literal items must use labels");
            }
        } while (cursor.match(PbsTokenKind.COMMA) && !cursor.check(PbsTokenKind.RIGHT_PAREN));
        return items;
    }

    private PbsAst.TupleItem parseTupleItem() {
        if (cursor.check(PbsTokenKind.IDENTIFIER) && cursor.checkNext(PbsTokenKind.COLON)) {
            final var label = cursor.advance();
            context.consume(PbsTokenKind.COLON, "Expected ':' after tuple label");
            final var value = expressionParserDelegate.parse();
            return new PbsAst.TupleItem(label.lexeme(), value, context.span(label.start(), value.span().getEnd()));
        }

        final var value = expressionParserDelegate.parse();
        return new PbsAst.TupleItem(null, value, value.span());
    }

    private PbsAst.Expression parseNewExpression(final PbsToken newToken) {
        final var typeName = context.consume(PbsTokenKind.IDENTIFIER, "Expected type name after 'new'");
        String ctorName = null;
        if (cursor.match(PbsTokenKind.DOT)) {
            ctorName = context.consume(PbsTokenKind.IDENTIFIER, "Expected constructor name after '.' in new target").lexeme();
        }

        context.consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after new target");
        final var arguments = new ArrayList<PbsAst.Expression>();
        if (!cursor.check(PbsTokenKind.RIGHT_PAREN)) {
            do {
                arguments.add(expressionParserDelegate.parse());
            } while (cursor.match(PbsTokenKind.COMMA));
        }
        final var close = context.consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after new arguments");
        return new PbsAst.NewExpr(
                typeName.lexeme(),
                ctorName,
                ReadOnlyList.wrap(arguments),
                context.span(newToken.start(), close.end()));
    }

    private PbsAst.Expression parseBindExpression(final PbsToken bindToken) {
        context.consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after 'bind'");
        final var contextExpression = expressionParserDelegate.parse();
        context.consume(PbsTokenKind.COMMA, "Expected ',' in bind(context, fn_name)");
        final var functionName = context.consume(PbsTokenKind.IDENTIFIER, "Expected function identifier in bind(context, fn_name)");
        final var close = context.consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after bind arguments");
        return new PbsAst.BindExpr(
                contextExpression,
                functionName.lexeme(),
                context.span(bindToken.start(), close.end()));
    }

    private PbsAst.Expression parseSomeExpression(final PbsToken someToken) {
        context.consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after 'some'");
        final var value = expressionParserDelegate.parse();
        final var close = context.consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after some(payload)");
        return new PbsAst.SomeExpr(value, context.span(someToken.start(), close.end()));
    }

    private PbsAst.Expression parseOkExpression(final PbsToken okToken) {
        context.consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after 'ok'");
        final var value = expressionParserDelegate.parse();
        final var close = context.consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after ok(payload)");
        return new PbsAst.OkExpr(value, context.span(okToken.start(), close.end()));
    }

    private PbsAst.Expression parseErrExpression(final PbsToken errToken) {
        context.consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after 'err'");
        final var errorPath = errorPathParserDelegate.parse();
        final var close = context.consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after err(Error.case)");
        return new PbsAst.ErrExpr(errorPath, context.span(errToken.start(), close.end()));
    }

}
