package p.studio.compiler.pbs.parser;

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

import java.util.ArrayList;

final class PbsTopLevelParser {
    private final PbsParserContext context;
    private final PbsTokenCursor cursor;
    private final PbsAttributeParser attributeParser;
    private final PbsDeclarationParser declarationParser;

    PbsTopLevelParser(
            final PbsParserContext context,
            final PbsAttributeParser attributeParser,
            final PbsDeclarationParser declarationParser) {
        this.context = context;
        this.cursor = context.cursor();
        this.attributeParser = attributeParser;
        this.declarationParser = declarationParser;
    }

    PbsAst.File parseFile() {
        final var imports = new ArrayList<PbsAst.ImportDecl>();
        final var topDecls = new ArrayList<PbsAst.TopDecl>();

        while (!cursor.isAtEnd()) {
            var pendingAttributes = ReadOnlyList.<PbsAst.Attribute>empty();
            if (cursor.check(PbsTokenKind.LEFT_BRACKET)) {
                pendingAttributes = attributeParser.parseAttributeList();
            }

            if (cursor.match(PbsTokenKind.IMPORT)) {
                rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "import declarations");
                imports.add(declarationParser.parseImport(cursor.previous()));
                continue;
            }

            if (cursor.match(PbsTokenKind.FN)) {
                topDecls.add(declarationParser.parseFunction(cursor.previous(), pendingAttributes));
                continue;
            }

            if (cursor.match(PbsTokenKind.DECLARE)) {
                topDecls.add(declarationParser.parseDeclareTopDecl(cursor.previous(), pendingAttributes));
                continue;
            }

            if (cursor.match(PbsTokenKind.IMPLEMENTS)) {
                rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "implements declarations");
                topDecls.add(declarationParser.parseImplementsDeclaration(cursor.previous()));
                continue;
            }

            if (cursor.match(PbsTokenKind.MOD, PbsTokenKind.PUB)) {
                rejectAttributesBeforeUnsupportedTopDecl(pendingAttributes, "barrel-only visibility modifiers");
                final var token = cursor.previous();
                context.report(token, ParseErrors.E_PARSE_VISIBILITY_IN_SOURCE,
                        "Visibility modifiers are barrel-only and cannot appear in .pbs declarations");
                topDecls.add(new PbsAst.InvalidDecl("visibility modifier in source", context.span(token.start(), token.end())));
                synchronizeTopLevel();
                continue;
            }

            if (cursor.check(PbsTokenKind.EOF)) {
                if (!pendingAttributes.isEmpty()) {
                    reportAttributesNotAllowed(pendingAttributes, "Attributes must precede a valid declaration");
                }
                break;
            }

            if (!pendingAttributes.isEmpty()) {
                reportAttributesNotAllowed(pendingAttributes, "Attributes must precede a valid declaration");
            }
            final var token = cursor.peek();
            context.report(token, ParseErrors.E_PARSE_UNEXPECTED_TOKEN,
                    "Expected top-level declaration ('fn', 'declare', 'implements') or import");
            topDecls.add(new PbsAst.InvalidDecl("unexpected top-level token", context.span(token.start(), token.end())));
            synchronizeTopLevel();
        }

        final var eof = cursor.peek();
        return new PbsAst.File(
                ReadOnlyList.wrap(imports),
                ReadOnlyList.wrap(topDecls),
                context.span(0, eof.end()));
    }

    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 isOrdinaryMode() {
        return context.parseMode() == PbsParser.ParseMode.ORDINARY;
    }

    private void synchronizeTopLevel() {
        while (!cursor.isAtEnd()) {
            if (isTopLevelRestartToken(cursor.peek().kind())) {
                return;
            }
            if (cursor.match(PbsTokenKind.SEMICOLON)) {
                return;
            }
            cursor.advance();
        }
    }

    private boolean isTopLevelRestartToken(final PbsTokenKind kind) {
        return kind == PbsTokenKind.FN
                || kind == PbsTokenKind.IMPORT
                || kind == PbsTokenKind.DECLARE
                || kind == PbsTokenKind.IMPLEMENTS
                || kind == PbsTokenKind.LEFT_BRACKET;
    }
}
