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.compiler.source.Span;
import p.studio.compiler.source.diagnostics.RelatedSpan;
import p.studio.utilities.structures.ReadOnlyList;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

final class PbsDeclarationParser {
    private static final boolean REQUIRE_SEMICOLON = true;

    @FunctionalInterface
    interface BlockParserDelegate {
        PbsAst.Block parse();
    }

    @FunctionalInterface
    interface DeclarationTerminatorDelegate {
        long consume();
    }

    @FunctionalInterface
    interface TopLevelSynchronizer {
        void synchronize();
    }

    private final PbsParserContext context;
    private final PbsTokenCursor cursor;
    private final PbsExprParser exprParser;
    private final PbsAttributeParser attributeParser;
    private final PbsTypeParser typeParser;
    private final BlockParserDelegate blockParserDelegate;
    private final DeclarationTerminatorDelegate declarationTerminatorDelegate;
    private final TopLevelSynchronizer topLevelSynchronizer;

    PbsDeclarationParser(
            final PbsParserContext context,
            final PbsExprParser exprParser,
            final PbsAttributeParser attributeParser,
            final PbsTypeParser typeParser,
            final BlockParserDelegate blockParserDelegate,
            final DeclarationTerminatorDelegate declarationTerminatorDelegate,
            final TopLevelSynchronizer topLevelSynchronizer) {
        this.context = context;
        this.cursor = context.cursor();
        this.exprParser = exprParser;
        this.attributeParser = attributeParser;
        this.typeParser = typeParser;
        this.blockParserDelegate = blockParserDelegate;
        this.declarationTerminatorDelegate = declarationTerminatorDelegate;
        this.topLevelSynchronizer = topLevelSynchronizer;
    }

    PbsAst.ImportDecl parseImport(final PbsToken importToken) {
        final var items = new ArrayList<PbsAst.ImportItem>();
        if (cursor.match(PbsTokenKind.LEFT_BRACE)) {
            while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) {
                if (cursor.match(PbsTokenKind.IDENTIFIER)) {
                    final var itemName = cursor.previous();
                    String alias = null;
                    if (cursor.match(PbsTokenKind.AS)) {
                        alias = consume(PbsTokenKind.IDENTIFIER, "Expected alias identifier after 'as'").lexeme();
                    }
                    items.add(new PbsAst.ImportItem(
                            itemName.lexeme(),
                            alias,
                            span(itemName.start(), cursor.previous().end())));
                    cursor.match(PbsTokenKind.COMMA);
                    continue;
                }
                report(cursor.peek(), ParseErrors.E_PARSE_UNEXPECTED_TOKEN, "Invalid import item");
                cursor.advance();
            }
            consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' in import list");
            consume(PbsTokenKind.FROM, "Expected 'from' in named import");
        }

        final var moduleRef = parseModuleRef();
        final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after import");
        return new PbsAst.ImportDecl(
                ReadOnlyList.wrap(items),
                moduleRef,
                span(importToken.start(), semicolon.end()));
    }

    PbsAst.ModuleRef parseModuleRef() {
        final var at = consume(PbsTokenKind.AT, "Expected '@' in module reference");
        final var project = consume(PbsTokenKind.IDENTIFIER, "Expected project identifier in module reference");
        consume(PbsTokenKind.COLON, "Expected ':' in module reference");
        final var segments = new ArrayList<String>();
        final var firstSegment = consume(PbsTokenKind.IDENTIFIER, "Expected module identifier");
        segments.add(firstSegment.lexeme());
        var end = firstSegment.end();
        while (cursor.match(PbsTokenKind.SLASH)) {
            final var segment = consume(PbsTokenKind.IDENTIFIER, "Expected module path segment after '/'");
            segments.add(segment.lexeme());
            end = segment.end();
        }
        return new PbsAst.ModuleRef(project.lexeme(), ReadOnlyList.wrap(segments), span(at.start(), end));
    }

    PbsAst.TopDecl parseDeclareTopDecl(
            final PbsToken declareToken,
            final ReadOnlyList<PbsAst.Attribute> pendingAttributes) {
        if (cursor.match(PbsTokenKind.STRUCT)) {
            rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "struct declarations");
            return parseStructDeclaration(declareToken);
        }
        if (cursor.match(PbsTokenKind.CONTRACT)) {
            rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "contract declarations");
            return parseContractDeclaration(declareToken);
        }
        if (cursor.match(PbsTokenKind.SERVICE)) {
            rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "service declarations");
            return parseServiceDeclaration(declareToken);
        }
        if (cursor.match(PbsTokenKind.ERROR)) {
            rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "error declarations");
            return parseErrorDeclaration(declareToken);
        }
        if (cursor.match(PbsTokenKind.ENUM)) {
            rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "enum declarations");
            return parseEnumDeclaration(declareToken);
        }
        if (cursor.match(PbsTokenKind.CALLBACK)) {
            rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "callback declarations");
            return parseCallbackDeclaration(declareToken);
        }
        if (cursor.match(PbsTokenKind.GLOBAL)) {
            rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "global declarations");
            return parseGlobalDeclaration(declareToken);
        }
        if (cursor.match(PbsTokenKind.CONST)) {
            return parseConstDeclaration(declareToken, pendingAttributes);
        }
        if (cursor.match(PbsTokenKind.HOST)) {
            rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "host declarations");
            if (isNotInterfaceMode()) {
                final var end = declarationTerminatorDelegate.consume();
                report(cursor.previous(), ParseErrors.E_PARSE_RESERVED_DECLARATION,
                        "'declare host' is reserved and not supported in ordinary source modules");
                return new PbsAst.InvalidDecl("reserved declare host", span(declareToken.start(), end));
            }
            return parseHostDeclaration(declareToken);
        }
        if (cursor.match(PbsTokenKind.BUILTIN)) {
            consume(PbsTokenKind.TYPE, "Expected 'type' in 'declare builtin type' declaration");
            if (isNotInterfaceMode()) {
                final var end = declarationTerminatorDelegate.consume();
                report(cursor.previous(), ParseErrors.E_PARSE_RESERVED_DECLARATION,
                        "'declare builtin type' is reserved and not supported in ordinary source modules");
                return new PbsAst.InvalidDecl("reserved declare builtin type", span(declareToken.start(), end));
            }
            return parseBuiltinTypeDeclaration(declareToken, pendingAttributes);
        }

        if (!pendingAttributes.isEmpty()) {
            reportAttributesNotAllowed(pendingAttributes, "Attributes are not valid for this declaration form");
        }

        report(cursor.peek(), ParseErrors.E_PARSE_UNEXPECTED_TOKEN,
                "Expected declaration kind after 'declare'");
        topLevelSynchronizer.synchronize();
        final var token = cursor.previous();
        return new PbsAst.InvalidDecl("invalid declare form", span(declareToken.start(), token.end()));
    }

    PbsAst.FunctionDecl parseFunction(
            final PbsToken fnToken,
            final ReadOnlyList<PbsAst.Attribute> pendingAttributes) {
        return parseFunctionLike(fnToken, pendingAttributes);
    }

    PbsAst.ImplementsDecl parseImplementsDeclaration(final PbsToken implementsToken) {
        final var contractName = consume(PbsTokenKind.IDENTIFIER, "Expected contract name in 'implements'");
        consume(PbsTokenKind.FOR, "Expected 'for' in 'implements' declaration");
        final var ownerName = consume(PbsTokenKind.IDENTIFIER, "Expected owner name after 'for'");
        consume(PbsTokenKind.USING, "Expected 'using' in 'implements' declaration");
        final var binderName = consume(PbsTokenKind.IDENTIFIER, "Expected binder name after 'using'");

        final var methods = new ArrayList<PbsAst.FunctionDecl>();
        long end;
        if (cursor.check(PbsTokenKind.LEFT_BRACE)) {
            cursor.advance();
            while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) {
                if (!cursor.match(PbsTokenKind.FN)) {
                    report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE,
                            "Implements body accepts only function declarations");
                    cursor.advance();
                    continue;
                }
                methods.add(parseFunctionLike(cursor.previous(), ReadOnlyList.empty()));
            }
            end = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end implements body").end();
        } else {
            report(cursor.peek(), ParseErrors.E_PARSE_EXPECTED_TOKEN,
                    "Expected '{' to start 'implements' body");
            end = declarationTerminatorDelegate.consume();
        }

        return new PbsAst.ImplementsDecl(
                contractName.lexeme(),
                ownerName.lexeme(),
                binderName.lexeme(),
                ReadOnlyList.wrap(methods),
                span(implementsToken.start(), end));
    }

    private PbsAst.HostDecl parseHostDeclaration(final PbsToken declareToken) {
        final var name = consume(PbsTokenKind.IDENTIFIER, "Expected host declaration name");
        consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start host declaration body");
        final var signatures = new ArrayList<PbsAst.FunctionSignature>();
        while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) {
            var attributes = ReadOnlyList.<PbsAst.Attribute>empty();
            if (cursor.check(PbsTokenKind.LEFT_BRACKET)) {
                attributes = attributeParser.parseAttributeList();
            }
            if (!cursor.match(PbsTokenKind.FN)) {
                report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE,
                        "Host declaration body accepts only function signatures");
                cursor.advance();
                continue;
            }
            signatures.add(parseFunctionSignature(cursor.previous(), attributes));
        }
        final var rightBrace = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end host declaration body");
        return new PbsAst.HostDecl(
                name.lexeme(),
                ReadOnlyList.wrap(signatures),
                span(declareToken.start(), rightBrace.end()));
    }

    private PbsAst.BuiltinTypeDecl parseBuiltinTypeDeclaration(
            final PbsToken declareToken,
            final ReadOnlyList<PbsAst.Attribute> declarationAttributes) {
        final var name = consume(PbsTokenKind.IDENTIFIER, "Expected builtin type name");
        consume(PbsTokenKind.LEFT_PAREN, "Expected '(' in builtin type declaration");
        final var fields = parseBuiltinTypeFields();
        consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after builtin type fields");

        consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start builtin type body");
        final var signatures = new ArrayList<PbsAst.FunctionSignature>();
        while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) {
            var attributes = ReadOnlyList.<PbsAst.Attribute>empty();
            if (cursor.check(PbsTokenKind.LEFT_BRACKET)) {
                attributes = attributeParser.parseAttributeList();
            }
            if (!cursor.match(PbsTokenKind.FN)) {
                report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE,
                        "Builtin type body accepts only function signatures");
                cursor.advance();
                continue;
            }
            signatures.add(parseFunctionSignature(cursor.previous(), attributes));
        }
        final var rightBrace = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end builtin type body");
        return new PbsAst.BuiltinTypeDecl(
                name.lexeme(),
                ReadOnlyList.wrap(fields),
                ReadOnlyList.wrap(signatures),
                declarationAttributes,
                span(declareToken.start(), rightBrace.end()));
    }

    private ArrayList<PbsAst.BuiltinFieldDecl> parseBuiltinTypeFields() {
        final var fields = new ArrayList<PbsAst.BuiltinFieldDecl>();
        if (cursor.check(PbsTokenKind.RIGHT_PAREN)) {
            return fields;
        }

        do {
            final var start = cursor.peek();
            var attributes = ReadOnlyList.<PbsAst.Attribute>empty();
            if (cursor.check(PbsTokenKind.LEFT_BRACKET)) {
                attributes = attributeParser.parseAttributeList();
            }

            var isPublic = false;
            if (cursor.match(PbsTokenKind.PUB)) {
                isPublic = true;
                if (cursor.match(PbsTokenKind.MUT)) {
                    final var mutToken = cursor.previous();
                    report(mutToken, ParseErrors.E_PARSE_INVALID_DECL_SHAPE,
                            "'mut' is not allowed in builtin type fields");
                }
            } else if (cursor.match(PbsTokenKind.MUT)) {
                final var mutToken = cursor.previous();
                report(mutToken, ParseErrors.E_PARSE_INVALID_DECL_SHAPE,
                        "'mut' is not allowed in builtin type fields");
            }

            final var fieldName = consume(PbsTokenKind.IDENTIFIER, "Expected builtin field name");
            consume(PbsTokenKind.COLON, "Expected ':' after builtin field name");
            final var typeRef = typeParser.parseTypeRef();
            fields.add(new PbsAst.BuiltinFieldDecl(
                    fieldName.lexeme(),
                    typeRef,
                    isPublic,
                    attributes,
                    span(start.start(), typeRef.span().getEnd())));
        } while (cursor.match(PbsTokenKind.COMMA) && !cursor.check(PbsTokenKind.RIGHT_PAREN));

        return fields;
    }

    private PbsAst.StructDecl parseStructDeclaration(final PbsToken declareToken) {
        final var name = consume(PbsTokenKind.IDENTIFIER, "Expected struct name");
        consume(PbsTokenKind.LEFT_PAREN, "Expected '(' in struct declaration");
        final var fields = parseStructFields();
        consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after struct fields");

        final var methods = new ArrayList<PbsAst.FunctionDecl>();
        final var ctors = new ArrayList<PbsAst.CtorDecl>();
        final boolean hasBody;
        long end;
        if (cursor.match(PbsTokenKind.SEMICOLON)) {
            hasBody = false;
            end = cursor.previous().end();
        } else if (cursor.match(PbsTokenKind.LEFT_BRACE)) {
            hasBody = true;
            final var bodyParse = parseStructBodyAndConsumeRightBrace(cursor.previous());
            methods.addAll(bodyParse.methods().asList());
            ctors.addAll(bodyParse.ctors().asList());
            end = bodyParse.end();
        } else {
            report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE,
                    "Struct declaration must end with ';' or contain a '{...}' body");
            hasBody = false;
            end = declarationTerminatorDelegate.consume();
        }

        return new PbsAst.StructDecl(
                name.lexeme(),
                ReadOnlyList.wrap(fields),
                ReadOnlyList.wrap(methods),
                ReadOnlyList.wrap(ctors),
                hasBody,
                span(declareToken.start(), end));
    }

    private ArrayList<PbsAst.StructField> parseStructFields() {
        final var fields = new ArrayList<PbsAst.StructField>();
        if (cursor.check(PbsTokenKind.RIGHT_PAREN)) {
            return fields;
        }

        do {
            final var start = cursor.peek();
            var isPublic = false;
            var isMutable = false;
            if (cursor.match(PbsTokenKind.PUB)) {
                isPublic = true;
                if (cursor.match(PbsTokenKind.MUT)) {
                    isMutable = true;
                }
            } else if (cursor.match(PbsTokenKind.MUT)) {
                final var mutToken = cursor.previous();
                report(mutToken, ParseErrors.E_PARSE_INVALID_DECL_SHAPE,
                        "'mut' is only valid immediately after 'pub' in struct fields");
            }

            final var fieldName = consume(PbsTokenKind.IDENTIFIER, "Expected struct field name");
            consume(PbsTokenKind.COLON, "Expected ':' after struct field name");
            final var typeRef = typeParser.parseTypeRef();
            fields.add(new PbsAst.StructField(
                    fieldName.lexeme(),
                    typeRef,
                    isPublic,
                    isMutable,
                    span(start.start(), typeRef.span().getEnd())));
        } while (cursor.match(PbsTokenKind.COMMA) && !cursor.check(PbsTokenKind.RIGHT_PAREN));

        return fields;
    }

    private StructBodyParse parseStructBodyAndConsumeRightBrace(final PbsToken leftBrace) {
        final var methods = new ArrayList<PbsAst.FunctionDecl>();
        final var ctors = new ArrayList<PbsAst.CtorDecl>();
        while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) {
            if (cursor.match(PbsTokenKind.FN)) {
                methods.add(parseFunctionLike(cursor.previous(), ReadOnlyList.empty()));
                continue;
            }
            if (cursor.match(PbsTokenKind.CTOR)) {
                ctors.add(parseCtorDeclarationInBody(cursor.previous()));
                continue;
            }
            report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE,
                    "Struct body accepts only 'fn' methods and 'ctor' declarations");
            cursor.advance();
        }
        final var rightBrace = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end struct body");
        return new StructBodyParse(ReadOnlyList.wrap(methods), ReadOnlyList.wrap(ctors), rightBrace.end());
    }

    private PbsAst.CtorDecl parseCtorDeclarationInBody(final PbsToken ctorToken) {
        final var name = consume(PbsTokenKind.IDENTIFIER, "Expected constructor name");
        consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after constructor name");
        final var parameters = typeParser.parseParametersUntilRightParen();
        consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after constructor parameters");
        final var body = parseBlock();
        return new PbsAst.CtorDecl(
                name.lexeme(),
                ReadOnlyList.wrap(parameters),
                body,
                span(ctorToken.start(), body.span().getEnd()));
    }

    private PbsAst.ContractDecl parseContractDeclaration(final PbsToken declareToken) {
        final var name = consume(PbsTokenKind.IDENTIFIER, "Expected contract name");
        consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start contract body");
        final var signatures = new ArrayList<PbsAst.FunctionSignature>();
        while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) {
            var attributes = ReadOnlyList.<PbsAst.Attribute>empty();
            if (cursor.check(PbsTokenKind.LEFT_BRACKET)) {
                attributes = attributeParser.parseAttributeList();
                if (isOrdinaryMode()) {
                    reportAttributesNotAllowed(attributes, "Attributes are not allowed in ordinary .pbs source modules");
                    attributes = ReadOnlyList.empty();
                }
            }
            if (!cursor.match(PbsTokenKind.FN)) {
                report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE,
                        "Contract body accepts only function signatures");
                cursor.advance();
                continue;
            }
            signatures.add(parseFunctionSignature(cursor.previous(), attributes));
        }
        final var rightBrace = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end contract body");
        return new PbsAst.ContractDecl(name.lexeme(), ReadOnlyList.wrap(signatures), span(declareToken.start(), rightBrace.end()));
    }

    private PbsAst.ServiceDecl parseServiceDeclaration(final PbsToken declareToken) {
        final var name = consume(PbsTokenKind.IDENTIFIER, "Expected service name");
        consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start service body");
        final var methods = new ArrayList<PbsAst.FunctionDecl>();
        while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) {
            if (!cursor.match(PbsTokenKind.FN)) {
                report(cursor.peek(), ParseErrors.E_PARSE_INVALID_DECL_SHAPE,
                        "Service body accepts only method declarations");
                cursor.advance();
                continue;
            }
            methods.add(parseFunctionLike(cursor.previous(), ReadOnlyList.empty()));
        }
        final var rightBrace = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end service body");
        return new PbsAst.ServiceDecl(name.lexeme(), ReadOnlyList.wrap(methods), span(declareToken.start(), rightBrace.end()));
    }

    private PbsAst.ErrorDecl parseErrorDeclaration(final PbsToken declareToken) {
        final var name = consume(PbsTokenKind.IDENTIFIER, "Expected error name");
        consume(PbsTokenKind.LEFT_BRACE, "Expected '{' to start error body");
        final var cases = new ArrayList<String>();
        while (!cursor.check(PbsTokenKind.RIGHT_BRACE) && !cursor.isAtEnd()) {
            final var caseToken = consume(PbsTokenKind.IDENTIFIER, "Expected error case label");
            cases.add(caseToken.lexeme());
            consume(PbsTokenKind.SEMICOLON, "Expected ';' after error case label");
        }
        final var rightBrace = consume(PbsTokenKind.RIGHT_BRACE, "Expected '}' to end error body");
        return new PbsAst.ErrorDecl(name.lexeme(), ReadOnlyList.wrap(cases), span(declareToken.start(), rightBrace.end()));
    }

    private PbsAst.EnumDecl parseEnumDeclaration(final PbsToken declareToken) {
        final var name = consume(PbsTokenKind.IDENTIFIER, "Expected enum name");
        consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after enum name");

        final var cases = new ArrayList<PbsAst.EnumCase>();
        final var caseLabels = new HashMap<String, Span>();
        final var explicitCaseIds = new HashMap<Long, Span>();
        var hasExplicitCases = false;
        var hasImplicitCases = false;
        if (!cursor.check(PbsTokenKind.RIGHT_PAREN)) {
            do {
                final var caseName = consume(PbsTokenKind.IDENTIFIER, "Expected enum case label");
                final var caseNameSpan = span(caseName.start(), caseName.end());
                Long explicitValue = null;
                if (cursor.match(PbsTokenKind.EQUAL)) {
                    final var intToken = consume(PbsTokenKind.INT_LITERAL, "Expected integer literal after '=' in enum case");
                    final var intTokenSpan = span(intToken.start(), intToken.end());
                    explicitValue = parseLongOrNull(intToken.lexeme());
                    hasExplicitCases = true;
                    if (explicitValue == null) {
                        report(intToken, ParseErrors.E_PARSE_INVALID_ENUM_FORM,
                                "Invalid explicit enum identifier");
                    } else {
                        final var firstIdSpan = explicitCaseIds.putIfAbsent(explicitValue, intTokenSpan);
                        if (firstIdSpan != null) {
                            p.studio.compiler.source.diagnostics.Diagnostics.error(context.diagnostics(),
                                    ParseErrors.E_PARSE_DUPLICATE_ENUM_CASE_ID.name(),
                                    "Duplicate explicit enum identifier '%s'".formatted(explicitValue),
                                    intTokenSpan,
                                    List.of(new RelatedSpan("First explicit enum identifier is here", firstIdSpan)));
                        }
                    }
                } else {
                    hasImplicitCases = true;
                }

                final var firstLabelSpan = caseLabels.putIfAbsent(caseName.lexeme(), caseNameSpan);
                if (firstLabelSpan != null) {
                    p.studio.compiler.source.diagnostics.Diagnostics.error(context.diagnostics(),
                            ParseErrors.E_PARSE_DUPLICATE_ENUM_CASE_LABEL.name(),
                            "Duplicate enum case label '%s'".formatted(caseName.lexeme()),
                            caseNameSpan,
                            List.of(new RelatedSpan("First enum case label is here", firstLabelSpan)));
                }

                cases.add(new PbsAst.EnumCase(caseName.lexeme(), explicitValue, span(caseName.start(), cursor.previous().end())));
            } while (cursor.match(PbsTokenKind.COMMA) && !cursor.check(PbsTokenKind.RIGHT_PAREN));
        }

        if (hasExplicitCases && hasImplicitCases) {
            report(name, ParseErrors.E_PARSE_INVALID_ENUM_FORM,
                    "Enum declarations cannot mix implicit and explicit case identifiers");
        }

        final var rightParen = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after enum cases");
        final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after enum declaration");
        return new PbsAst.EnumDecl(name.lexeme(), ReadOnlyList.wrap(cases), span(declareToken.start(), Math.max(rightParen.end(), semicolon.end())));
    }

    private PbsAst.FunctionDecl parseFunctionLike(
            final PbsToken fnToken,
            final ReadOnlyList<PbsAst.Attribute> pendingAttributes) {
        final var lifecycleMarker = validateAndResolveLifecycleMarker(pendingAttributes);
        final var name = consumeCallableName();
        consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after function name");
        final var parameters = typeParser.parseParametersUntilRightParen();
        final var rightParen = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after parameter list");

        final var returnSpec = typeParser.parseCallableReturnSpec(rightParen);

        final var body = parseBlock();
        return new PbsAst.FunctionDecl(
                name.lexeme(),
                ReadOnlyList.wrap(parameters),
                returnSpec.kind(),
                returnSpec.returnType(),
                returnSpec.resultErrorType(),
                lifecycleMarker,
                body,
                span(fnToken.start(), body.span().getEnd()));
    }

    private PbsAst.FunctionSignature parseFunctionSignature(
            final PbsToken fnToken,
            final ReadOnlyList<PbsAst.Attribute> attributes) {
        final var name = consumeCallableName();
        consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after function name");
        final var parameters = typeParser.parseParametersUntilRightParen();
        final var rightParen = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after parameter list");

        final var returnSpec = typeParser.parseCallableReturnSpec(rightParen);

        long end = returnSpec.returnType().span().getEnd();
        if (REQUIRE_SEMICOLON) {
            end = consume(PbsTokenKind.SEMICOLON, "Expected ';' after function signature").end();
        }

        return new PbsAst.FunctionSignature(
                name.lexeme(),
                ReadOnlyList.wrap(parameters),
                returnSpec.kind(),
                returnSpec.returnType(),
                returnSpec.resultErrorType(),
                attributes,
                span(fnToken.start(), end));
    }

    private PbsToken consumeCallableName() {
        if (cursor.check(PbsTokenKind.IDENTIFIER)) {
            return cursor.advance();
        }
        final var token = cursor.peek();
        report(token, ParseErrors.E_PARSE_EXPECTED_TOKEN, "Expected a function name, but found " + token.kind());
        if (!cursor.isAtEnd()) {
            return cursor.advance();
        }
        return token;
    }

    private PbsAst.CallbackDecl parseCallbackDeclaration(final PbsToken declareToken) {
        final var name = consume(PbsTokenKind.IDENTIFIER, "Expected callback name");
        consume(PbsTokenKind.LEFT_PAREN, "Expected '(' after callback name");
        final var parameters = typeParser.parseParametersUntilRightParen();
        final var rightParen = consume(PbsTokenKind.RIGHT_PAREN, "Expected ')' after callback parameter list");

        final var returnSpec = typeParser.parseCallableReturnSpec(rightParen);

        final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after callback declaration");
        return new PbsAst.CallbackDecl(
                name.lexeme(),
                ReadOnlyList.wrap(parameters),
                returnSpec.kind(),
                returnSpec.returnType(),
                returnSpec.resultErrorType(),
                span(declareToken.start(), semicolon.end()));
    }

    private PbsAst.ConstDecl parseConstDeclaration(
            final PbsToken declareToken,
            final ReadOnlyList<PbsAst.Attribute> attributes) {
        final var name = consume(PbsTokenKind.IDENTIFIER, "Expected constant name");
        consume(PbsTokenKind.COLON, "Expected ':' after constant name");
        final var typeRef = typeParser.parseTypeRef();

        PbsAst.Expression initializer = null;
        if (cursor.match(PbsTokenKind.EQUAL)) {
            initializer = exprParser.parseExpression();
        }

        final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after constant declaration");
        return new PbsAst.ConstDecl(
                name.lexeme(),
                typeRef,
                initializer,
                attributes,
                span(declareToken.start(), semicolon.end()));
    }

    private PbsAst.GlobalDecl parseGlobalDeclaration(final PbsToken declareToken) {
        final var name = consume(PbsTokenKind.IDENTIFIER, "Expected global name");
        consume(PbsTokenKind.COLON, "Expected ':' after global name");
        final var explicitType = typeParser.parseTypeRef();
        consume(PbsTokenKind.EQUAL, "Expected '=' after global type annotation");
        final var initializer = exprParser.parseExpression();
        final var semicolon = consume(PbsTokenKind.SEMICOLON, "Expected ';' after global declaration");
        return new PbsAst.GlobalDecl(
                name.lexeme(),
                explicitType,
                initializer,
                span(declareToken.start(), semicolon.end()));
    }

    private PbsAst.Block parseBlock() {
        return blockParserDelegate.parse();
    }

    private void rejectAttributesBeforeUnsupportedTopDecl(
            final ReadOnlyList<PbsAst.Attribute> attributes,
            final String targetSurface) {
        if (attributes.isEmpty()) {
            return;
        }
        reportAttributesNotAllowed(attributes, "Attributes are not allowed before " + targetSurface);
    }

    private void reportAttributesNotAllowed(
            final ReadOnlyList<PbsAst.Attribute> attributes,
            final String message) {
        for (final var attribute : attributes) {
            p.studio.compiler.source.diagnostics.Diagnostics.error(context.diagnostics(),
                    ParseErrors.E_PARSE_ATTRIBUTES_NOT_ALLOWED.name(),
                    message,
                    attribute.span());
        }
    }

    private boolean isNotInterfaceMode() {
        return context.parseMode() != PbsParser.ParseMode.INTERFACE_MODULE;
    }

    private boolean isOrdinaryMode() {
        return context.parseMode() == PbsParser.ParseMode.ORDINARY;
    }

    private PbsAst.LifecycleMarker validateAndResolveLifecycleMarker(
            final ReadOnlyList<PbsAst.Attribute> attributes) {
        if (attributes.isEmpty()) {
            return PbsAst.LifecycleMarker.NONE;
        }

        if (!isOrdinaryMode()) {
            reportAttributesNotAllowed(attributes, "Attributes are not allowed before top-level functions");
            return PbsAst.LifecycleMarker.NONE;
        }

        PbsAst.LifecycleMarker marker = PbsAst.LifecycleMarker.NONE;
        for (final var attribute : attributes) {
            final PbsAst.LifecycleMarker nextMarker;
            if ("Init".equals(attribute.name())) {
                nextMarker = PbsAst.LifecycleMarker.INIT;
            } else if ("Frame".equals(attribute.name())) {
                nextMarker = PbsAst.LifecycleMarker.FRAME;
            } else {
                reportAttributesNotAllowed(attributes, "Only marker attributes [Init] and [Frame] are allowed before top-level functions");
                return PbsAst.LifecycleMarker.NONE;
            }

            if (!attribute.arguments().isEmpty()) {
                reportAttributesNotAllowed(attributes, "Lifecycle markers do not accept arguments");
                return PbsAst.LifecycleMarker.NONE;
            }

            if (marker != PbsAst.LifecycleMarker.NONE && marker != nextMarker) {
                reportAttributesNotAllowed(attributes, "Top-level functions cannot combine [Init] and [Frame]");
                return PbsAst.LifecycleMarker.NONE;
            }
            marker = nextMarker;
        }
        return marker;
    }

    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 Span span(final long start, final long end) {
        return context.span(start, end);
    }

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

    private record StructBodyParse(
            ReadOnlyList<PbsAst.FunctionDecl> methods,
            ReadOnlyList<PbsAst.CtorDecl> ctors,
            long end) {
    }
}
