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;
import java.util.function.Predicate;

final class PbsAttributeParser {
    private final PbsParserContext context;
    private final PbsTokenCursor cursor;
    private final Predicate<PbsTokenKind> topLevelRestartPredicate;

    PbsAttributeParser(
            final PbsParserContext context,
            final Predicate<PbsTokenKind> topLevelRestartPredicate) {
        this.context = context;
        this.cursor = context.cursor();
        this.topLevelRestartPredicate = topLevelRestartPredicate;
    }

    ReadOnlyList<PbsAst.Attribute> parseAttributeList() {
        final var attributes = new ArrayList<PbsAst.Attribute>();
        while (cursor.check(PbsTokenKind.LEFT_BRACKET)) {
            attributes.add(parseAttribute());
        }
        return ReadOnlyList.wrap(attributes);
    }

    private PbsAst.Attribute parseAttribute() {
        final var leftBracket = consume(PbsTokenKind.LEFT_BRACKET, "Expected '[' to start attribute");
        final var name = consume(PbsTokenKind.IDENTIFIER, "Expected attribute identifier");
        final var arguments = new ArrayList<PbsAst.AttributeArgument>();
        if (cursor.match(PbsTokenKind.LEFT_PAREN)) {
            parseAttributeArguments(arguments);
        }
        if ("Slot".equals(name.lexeme())) {
            report(name, ParseErrors.E_PARSE_INVALID_DECL_SHAPE,
                    "Attribute 'Slot' is not part of PBS syntax; builtin slots are inferred by the compiler");
        }

        long end = Math.max(leftBracket.end(), name.end());
        if (cursor.match(PbsTokenKind.RIGHT_BRACKET)) {
            end = cursor.previous().end();
        } else {
            report(cursor.peek(), ParseErrors.E_PARSE_EXPECTED_TOKEN, "Expected ']' to close attribute");
            end = recoverUntilAttributeCloseOrTopLevel(end);
        }
        return new PbsAst.Attribute(
                name.lexeme(),
                ReadOnlyList.wrap(arguments),
                context.span(leftBracket.start(), end));
    }

    private void parseAttributeArguments(final ArrayList<PbsAst.AttributeArgument> arguments) {
        if (!cursor.check(PbsTokenKind.RIGHT_PAREN)) {
            do {
                arguments.add(parseAttributeArgument());
            } while (cursor.match(PbsTokenKind.COMMA) && !cursor.check(PbsTokenKind.RIGHT_PAREN));
        }

        if (cursor.match(PbsTokenKind.RIGHT_PAREN)) {
            return;
        }
        report(cursor.peek(), ParseErrors.E_PARSE_EXPECTED_TOKEN, "Expected ')' after attribute arguments");
        recoverUntilAttributeArgumentClose();
    }

    private PbsAst.AttributeArgument parseAttributeArgument() {
        final var argumentStart = cursor.peek();
        final var name = consume(PbsTokenKind.IDENTIFIER, "Expected attribute argument name");
        consume(PbsTokenKind.EQUAL, "Expected '=' in attribute argument");
        final var value = parseAttributeValue();
        return new PbsAst.AttributeArgument(
                name.lexeme(),
                value,
                context.span(argumentStart.start(), value.span().getEnd()));
    }

    private PbsAst.AttributeValue parseAttributeValue() {
        if (cursor.match(PbsTokenKind.STRING_LITERAL)) {
            final var token = cursor.previous();
            return new PbsAst.AttributeStringValue(token.lexeme(), context.span(token.start(), token.end()));
        }
        if (cursor.match(PbsTokenKind.INT_LITERAL)) {
            final var token = cursor.previous();
            final var parsedLong = parseLongOrNull(token.lexeme());
            if (parsedLong != null) {
                return new PbsAst.AttributeIntValue(parsedLong, context.span(token.start(), token.end()));
            }
            report(token, ParseErrors.E_PARSE_EXPECTED_TOKEN, "Invalid integer literal in attribute argument");
            return new PbsAst.AttributeErrorValue(token.lexeme(), context.span(token.start(), token.end()));
        }
        if (cursor.match(PbsTokenKind.TRUE)) {
            final var token = cursor.previous();
            return new PbsAst.AttributeBoolValue(true, context.span(token.start(), token.end()));
        }
        if (cursor.match(PbsTokenKind.FALSE)) {
            final var token = cursor.previous();
            return new PbsAst.AttributeBoolValue(false, context.span(token.start(), token.end()));
        }

        final var token = cursor.peek();
        report(token, ParseErrors.E_PARSE_EXPECTED_TOKEN, "Expected attribute value (string, int, or bool)");
        if (!cursor.isAtEnd()
                && !cursor.check(PbsTokenKind.COMMA)
                && !cursor.check(PbsTokenKind.RIGHT_PAREN)
                && !cursor.check(PbsTokenKind.RIGHT_BRACKET)) {
            cursor.advance();
        }
        return new PbsAst.AttributeErrorValue("<error>", context.span(token.start(), token.end()));
    }

    private long recoverUntilAttributeCloseOrTopLevel(final long fallbackEnd) {
        long end = fallbackEnd;
        while (!cursor.isAtEnd()) {
            if (cursor.match(PbsTokenKind.RIGHT_BRACKET)) {
                return cursor.previous().end();
            }
            if (topLevelRestartPredicate.test(cursor.peek().kind())) {
                return end;
            }
            end = cursor.advance().end();
        }
        return end;
    }

    private void recoverUntilAttributeArgumentClose() {
        while (!cursor.isAtEnd()) {
            if (cursor.match(PbsTokenKind.RIGHT_PAREN)) {
                return;
            }
            if (cursor.check(PbsTokenKind.RIGHT_BRACKET) || topLevelRestartPredicate.test(cursor.peek().kind())) {
                return;
            }
            cursor.advance();
        }
    }

    private PbsToken consume(final PbsTokenKind kind, final String message) {
        if (cursor.check(kind)) {
            return cursor.advance();
        }
        final var token = cursor.peek();
        report(token, ParseErrors.E_PARSE_EXPECTED_TOKEN, message + ", found " + token.kind());
        if (!cursor.isAtEnd()) {
            return cursor.advance();
        }
        return token;
    }

    private void report(final PbsToken token, final ParseErrors parseErrors, final String message) {
        context.report(token, parseErrors, message);
    }

    private Long parseLongOrNull(final String text) {
        try {
            return Long.parseLong(text);
        } catch (NumberFormatException ignored) {
            return null;
        }
    }
}
