/* eslint-disable camelcase */
/* eslint-disable no-underscore-dangle */
import {
    ASTMain,
    ASTStmtBlock,
    N_DefFunction,
    N_DefInteractiveProgram,
    N_DefProcedure,
    N_DefProgram,
    N_DefType,
    N_ExprChoose,
    N_ExprConstantNumber,
    N_ExprConstantString,
    N_ExprFunctionCall,
    N_ExprList,
    N_ExprMatching,
    N_ExprRange,
    N_ExprStructure,
    N_ExprStructureUpdate,
    N_ExprTuple,
    N_ExprVariable,
    N_PatternNumber,
    N_PatternStructure,
    N_PatternTimeout,
    N_PatternTuple,
    N_PatternVariable,
    N_PatternWildcard,
    N_StmtAssignTuple,
    N_StmtAssignVariable,
    N_StmtBlock,
    N_StmtForeach,
    N_StmtIf,
    N_StmtProcedureCall,
    N_StmtRepeat,
    N_StmtReturn,
    N_StmtSwitch,
    N_StmtWhile
} from './ast';
import { LocalIndex, LocalParameter, LocalVariable, SymbolTable } from './symtable';
import { i18n, i18nF } from './i18n';

import { GbsSyntaxError } from './exceptions';
import { RecursionChecker } from './recursion_checker';
import { SourceReader } from './reader';

// Only for typing purposes
// eslint-disable-next-line @typescript-eslint/ban-types
const toStr = (x: string | Function): string => x as string;

const isBlockWithReturn = (stmt: ASTStmtBlock): boolean =>
    stmt.tag === N_StmtBlock &&
    stmt.statements.length > 0 &&
    stmt.statements.slice(-1)[0].tag === N_StmtReturn;

function fail(startPos: SourceReader, endPos: SourceReader, reason: string, args: any[]): void {
    throw new GbsSyntaxError(startPos, endPos, reason, args);
}

/* A semantic analyzer receives
 *   a symbol table (instance of SymbolTable)
 *   an abstract syntax tree (the output of a parser)
 *
 * Then:
 *
 * - It performs semantic checks (linting) to ensure that the
 *   program is well-formed.
 *
 * - It builds a symbol table with information on global identifiers
 *   such as procedures, functions, types, constructors, and fields.
 *
 * - The semantic analysis is structured as a recursive visit over the
 *   AST.
 *
 * We assume that the AST is the valid output of a parser.
 */
export class Linter {
    private _symtable: SymbolTable;
    private _enabledLinterChecks: Record<string, boolean>;

    public constructor(symtable: SymbolTable) {
        this._symtable = symtable;

        /* All checks performed by the linter have an entry in this dictionary.
         * The value of a check indicates whether it is enabled (true) or
         * disabled (false).
         *
         * If a check is disabled, it does not produce a syntax error.
         */
        this._enabledLinterChecks = {
            // Linter options
            'source-should-have-a-program-definition': true,
            'procedure-should-not-have-return': true,
            'function-should-have-return': true,
            'return-statement-not-allowed-here': true,
            'wildcard-pattern-should-be-last': true,
            'variable-pattern-should-be-last': true,
            'structure-pattern-repeats-constructor': true,
            'structure-pattern-repeats-tuple-arity': true,
            'structure-pattern-repeats-timeout': true,
            'pattern-does-not-match-type': true,
            'patterns-in-interactive-program-must-be-events': true,
            'patterns-in-interactive-program-cannot-be-variables': true,
            'patterns-in-switch-must-not-be-events': true,
            'patterns-in-foreach-must-not-be-events': true,
            'repeated-variable-in-tuple-assignment': true,
            'constructor-used-as-procedure': true,
            'undefined-procedure': true,
            'procedure-arity-mismatch': true,
            'numeric-pattern-repeats-number': true,
            'structure-pattern-arity-mismatch': true,
            'structure-construction-repeated-field': true,
            'structure-construction-invalid-field': true,
            'structure-construction-missing-field': true,
            'structure-construction-cannot-be-an-event': true,
            'undefined-function': true,
            'function-arity-mismatch': true,
            'type-used-as-constructor': true,
            'procedure-used-as-constructor': true,
            'undeclared-constructor': true,
            // Extensions
            'forbidden-extension-destructuring-foreach': true,
            'forbidden-extension-allow-recursion': true
        };
    }

    public lint(ast: ASTMain): SymbolTable {
        this._lintMain(ast);
        return this._symtable;
    }

    public _ensureLintCheckExists(linterCheckId: string): void {
        if (!(linterCheckId in this._enabledLinterChecks)) {
            throw Error('Linter check "' + linterCheckId + '" does not exist.');
        }
    }

    public enableCheck(linterCheckId: string, enabled: boolean): void {
        this._ensureLintCheckExists(linterCheckId);
        this._enabledLinterChecks[linterCheckId] = enabled;
    }

    public _lintCheck(
        startPos: SourceReader,
        endPos: SourceReader,
        reason: string,
        args: any[]
    ): void {
        this._ensureLintCheckExists(reason);
        if (this._enabledLinterChecks[reason]) {
            fail(startPos, endPos, reason, args);
        }
    }

    public _lintMain(ast: ASTMain): void {
        /* Collect all definitions into the symbol table.
         * This should be done all together, before linting individual
         * definitions, so all the names of types, constructors, fields, etc.
         * are already known when checking statements and expressions. */
        for (const definition of ast.definitions) {
            this._addDefinitionToSymbolTable(definition);
        }

        /* The source should either be empty or have exactly one program */
        if (ast.definitions.length > 0 && this._symtable.program === undefined) {
            this._lintCheck(
                ast.startPos,
                ast.endPos,
                'source-should-have-a-program-definition',
                []
            );
        }

        /* Lint individual definitions */
        for (const definition of ast.definitions) {
            this._lintDefinition(definition);
        }

        /* Disable recursion */
        this._disableRecursion(ast);
    }

    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
    public _addDefinitionToSymbolTable(definition: any): void {
        switch (definition.tag) {
            case N_DefProgram:
                return this._symtable.defProgram(definition);
            case N_DefInteractiveProgram:
                return this._symtable.defInteractiveProgram(definition);
            case N_DefProcedure:
                return this._symtable.defProcedure(definition);
            case N_DefFunction:
                return this._symtable.defFunction(definition);
            case N_DefType:
                return this._symtable.defType(definition);
            default:
                throw Error('Unknown definition: ' + Symbol.keyFor(definition.tag));
        }
    }

    /** Definitions **/

    public _lintDefinition(definition: { tag: symbol }): void {
        switch (definition.tag) {
            case N_DefProgram:
                return this._lintDefProgram(definition);
            case N_DefInteractiveProgram:
                return this._lintDefInteractiveProgram(definition);
            case N_DefProcedure:
                return this._lintDefProcedure(definition);
            case N_DefFunction:
                return this._lintDefFunction(definition);
            case N_DefType:
                return this._lintDefType();
            default:
                throw Error('Linter: Definition not implemented: ' + Symbol.keyFor(definition.tag));
        }
    }

    public _lintDefProgram(definition: { tag?: symbol; body?: any }): void {
        /* Lint body */
        this._lintStmtBlock(definition.body, true /* allowReturn */);

        /* Remove all local names */
        this._symtable.exitScope();
    }

    public _lintDefInteractiveProgram(definition: { tag?: symbol; branches?: any }): void {
        /* Lint all branches */
        this._lintSwitchBranches(definition.branches, true /* isInteractiveProgram */);
    }

    public _lintDefProcedure(definition: {
        tag?: symbol;
        body?: any;
        startPos?: any;
        endPos?: any;
        name?: any;
        parameters?: any;
    }): void {
        /* Check that it does not have a return statement */
        if (isBlockWithReturn(definition.body)) {
            this._lintCheck(
                definition.startPos,
                definition.endPos,
                'procedure-should-not-have-return',
                [definition.name.value]
            );
        }

        /* Add parameters as local names */
        for (const parameter of definition.parameters) {
            this._symtable.addNewLocalName(parameter, LocalParameter);
        }

        /* Lint body */
        this._lintStmtBlock(definition.body, false /* !allowReturn */);

        /* Remove all local names */
        this._symtable.exitScope();
    }

    public _lintDefFunction(definition: {
        tag?: symbol;
        body?: any;
        startPos?: any;
        endPos?: any;
        name?: any;
        parameters?: any;
    }): void {
        /* Check that it has a return statement */
        if (!isBlockWithReturn(definition.body)) {
            this._lintCheck(definition.startPos, definition.endPos, 'function-should-have-return', [
                definition.name.value
            ]);
        }

        /* Add parameters as local names */
        for (const parameter of definition.parameters) {
            this._symtable.addNewLocalName(parameter, LocalParameter);
        }

        /* Lint body */
        this._lintStmtBlock(definition.body, true /* allowReturn */);

        /* Remove all local names */
        this._symtable.exitScope();
    }

    public _lintDefType(): void {
        /* No restrictions */
    }

    /** Statements **/

    public _lintStatement(statement: { tag: symbol }): void {
        switch (statement.tag) {
            case N_StmtBlock:
                /* Do not allow return in nested blocks */
                return this._lintStmtBlock(statement, false /* !allowReturn */);
            case N_StmtReturn:
                return this._lintStmtReturn(statement);
            case N_StmtIf:
                return this._lintStmtIf(statement);
            case N_StmtRepeat:
                return this._lintStmtRepeat(statement);
            case N_StmtForeach:
                return this._lintStmtForeach(statement);
            case N_StmtWhile:
                return this._lintStmtWhile(statement);
            case N_StmtSwitch:
                return this._lintStmtSwitch(statement);
            case N_StmtAssignVariable:
                return this._lintStmtAssignVariable(statement);
            case N_StmtAssignTuple:
                return this._lintStmtAssignTuple(statement);
            case N_StmtProcedureCall:
                return this._lintStmtProcedureCall(statement);
            default:
                throw Error('Linter: Statement not implemented: ' + Symbol.keyFor(statement.tag));
        }
    }

    public _lintStmtBlock(block: { tag?: symbol; statements?: any }, allowReturn: boolean): void {
        let i = 0;
        for (const statement of block.statements) {
            const returnAllowed = allowReturn && i === block.statements.length - 1;
            if (!returnAllowed && statement.tag === N_StmtReturn) {
                this._lintCheck(
                    statement.startPos,
                    statement.endPos,
                    'return-statement-not-allowed-here',
                    []
                );
            }
            this._lintStatement(statement);
            i++;
        }
    }

    public _lintStmtReturn(statement: { tag?: symbol; result?: any }): void {
        this._lintExpression(statement.result);
    }

    public _lintStmtIf(statement: {
        tag?: symbol;
        condition?: any;
        thenBlock?: any;
        elseBlock?: any;
    }): void {
        this._lintExpression(statement.condition);
        this._lintStatement(statement.thenBlock);
        if (statement.elseBlock !== undefined) {
            this._lintStatement(statement.elseBlock);
        }
    }

    public _lintStmtRepeat(statement: { tag?: symbol; times?: any; body?: any }): void {
        this._lintExpression(statement.times);
        this._lintStatement(statement.body);
    }

    public _lintStmtForeach(statement: {
        tag?: symbol;
        pattern?: any;
        range?: any;
        body?: any;
    }): void {
        this._lintStmtForeachPattern(statement.pattern);
        this._lintExpression(statement.range);
        for (const variable of statement.pattern.boundVariables) {
            this._symtable.addNewLocalName(variable, LocalIndex);
        }
        this._lintStatement(statement.body);
        for (const variable of statement.pattern.boundVariables) {
            this._symtable.removeLocalName(variable);
        }
    }

    public _lintStmtForeachPattern(pattern: {
        tag?: any;
        startPos: any;
        endPos: any;
        constructorName: any;
        boundVariables: any;
    }): void {
        /* If "DestructuringForeach" is disabled, forbid complex patterns.
         * Allow only variable patterns (indices). */
        if (pattern.tag !== N_PatternVariable) {
            this._lintCheck(
                pattern.startPos,
                pattern.endPos,
                'forbidden-extension-destructuring-foreach',
                []
            );
        }

        /* Check that the pattern itself is well-formed */
        this._lintPattern(pattern);

        /* The pattern in a foreach cannot be an event */
        const patternType = this._patternType(pattern);
        if (patternType === i18n('TYPE:Event')) {
            this._lintCheck(
                pattern.startPos,
                pattern.endPos,
                'patterns-in-foreach-must-not-be-events',
                []
            );
        }
    }

    public _lintStmtWhile(statement: { tag?: symbol; condition?: any; body?: any }): void {
        this._lintExpression(statement.condition);
        this._lintStatement(statement.body);
    }

    public _lintStmtSwitch(statement: { tag?: symbol; subject?: any; branches?: any }): void {
        this._lintExpression(statement.subject);
        this._lintSwitchBranches(statement.branches, false /* !isInteractiveProgram */);
    }

    public _lintSwitchBranches(branches: any[], isInteractiveProgram: boolean): void {
        this._lintBranches(branches, isInteractiveProgram, false /* isMatching */);
    }

    public _lintBranches(
        branches: any[],
        isInteractiveProgram: boolean,
        isMatching: boolean
    ): void {
        /* Check that each pattern is well-formed */
        for (const branch of branches) {
            this._lintPattern(branch.pattern);
        }

        this._branchesCheckWildcardAndVariable(branches);
        this._branchesCheckNoRepeats(branches);
        this._branchesCheckCompatible(branches);
        if (isInteractiveProgram) {
            this._branchesCheckTypeEvent(branches);
        } else {
            this._branchesCheckTypeNotEvent(branches);
        }

        /* Lint recursively each branch */
        for (const branch of branches) {
            this._lintBranchBody(branch, isMatching);
        }
    }

    /* Check that there is at most one wildcard/variable pattern at the end */
    public _branchesCheckWildcardAndVariable(branches: any[]): void {
        let i = 0;
        const n = branches.length;
        for (const branch of branches) {
            if (branch.pattern.tag === N_PatternWildcard && i !== n - 1) {
                this._lintCheck(
                    branch.pattern.startPos,
                    branch.pattern.endPos,
                    'wildcard-pattern-should-be-last',
                    []
                );
            }
            if (branch.pattern.tag === N_PatternVariable && i !== n - 1) {
                this._lintCheck(
                    branch.pattern.startPos,
                    branch.pattern.endPos,
                    'variable-pattern-should-be-last',
                    [branch.pattern.variableName.value]
                );
            }
            i++;
        }
    }

    /* Check that there are no repeated constructors in a sequence
     * of branches. */
    public _branchesCheckNoRepeats(branches: any[]): void {
        const coveredNumbers = {};
        const coveredConstructors = {};
        const coveredTuples = {};
        let coveredTimeout = false;
        for (const branch of branches) {
            switch (branch.pattern.tag) {
                case N_PatternWildcard:
                case N_PatternVariable:
                    /* Already checked in _switchBranchesCheckWildcardAndVariable */
                    break;
                case N_PatternNumber: {
                    const number = branch.pattern.number.value;
                    if (number in coveredNumbers) {
                        this._lintCheck(
                            branch.pattern.startPos,
                            branch.pattern.endPos,
                            'numeric-pattern-repeats-number',
                            [number]
                        );
                    }
                    coveredNumbers[number] = true;
                    break;
                }
                case N_PatternStructure: {
                    const constructorName = branch.pattern.constructorName.value;
                    if (constructorName in coveredConstructors) {
                        this._lintCheck(
                            branch.pattern.startPos,
                            branch.pattern.endPos,
                            'structure-pattern-repeats-constructor',
                            [constructorName]
                        );
                    }
                    coveredConstructors[constructorName] = true;
                    break;
                }
                case N_PatternTuple: {
                    const arity = branch.pattern.boundVariables.length;
                    if (arity in coveredTuples) {
                        this._lintCheck(
                            branch.pattern.startPos,
                            branch.pattern.endPos,
                            'structure-pattern-repeats-tuple-arity',
                            [arity]
                        );
                    }
                    coveredTuples[arity] = true;
                    break;
                }
                case N_PatternTimeout: {
                    if (coveredTimeout) {
                        this._lintCheck(
                            branch.pattern.startPos,
                            branch.pattern.endPos,
                            'structure-pattern-repeats-timeout',
                            []
                        );
                    }
                    coveredTimeout = true;
                    break;
                }
                default:
                    throw Error(
                        'Linter: pattern "' +
                            Symbol.keyFor(branch.pattern.tag) +
                            '" not implemented.'
                    );
            }
        }
    }

    /* Check that constructors are compatible,
     * i.e. that they belong to the same type */
    public _branchesCheckCompatible(branches: any[]): void {
        let expectedType;
        for (const branch of branches) {
            const patternType = this._patternType(branch.pattern);
            if (expectedType === undefined) {
                expectedType = patternType;
            } else if (patternType !== undefined && expectedType !== patternType) {
                this._lintCheck(
                    branch.pattern.startPos,
                    branch.pattern.endPos,
                    'pattern-does-not-match-type',
                    [i18nF('<pattern-type>')(expectedType), i18nF('<pattern-type>')(patternType)]
                );
            }
        }
    }

    /* Check that there are patterns are of type Event */
    public _branchesCheckTypeEvent(branches: any[]): void {
        for (const branch of branches) {
            const patternType = this._patternType(branch.pattern);
            if (patternType !== undefined && patternType !== i18n('TYPE:Event')) {
                this._lintCheck(
                    branch.pattern.startPos,
                    branch.pattern.endPos,
                    'patterns-in-interactive-program-must-be-events',
                    []
                );
            }
            if (branch.pattern.tag === N_PatternVariable) {
                this._lintCheck(
                    branch.pattern.startPos,
                    branch.pattern.endPos,
                    'patterns-in-interactive-program-cannot-be-variables',
                    []
                );
            }
        }
    }

    /* Check that there are no patterns of type Event */
    public _branchesCheckTypeNotEvent(branches: any[]): void {
        for (const branch of branches) {
            const patternType = this._patternType(branch.pattern);
            if (patternType === i18n('TYPE:Event')) {
                this._lintCheck(
                    branch.pattern.startPos,
                    branch.pattern.endPos,
                    'patterns-in-switch-must-not-be-events',
                    []
                );
            }
        }
    }

    /* Recursively lint the body of each branch. Locally bind variables. */
    public _lintBranchBody(
        branch: { pattern: { boundVariables: any }; body: { tag: symbol } },
        isMatching: boolean
    ): void {
        for (const variable of branch.pattern.boundVariables) {
            this._symtable.addNewLocalName(variable, LocalParameter);
        }
        if (isMatching) {
            this._lintExpression(branch.body);
        } else {
            this._lintStatement(branch.body);
        }
        for (const variable of branch.pattern.boundVariables) {
            this._symtable.removeLocalName(variable);
        }
    }

    /* Return a description of the type of a pattern */
    public _patternType(pattern: {
        tag?: any;
        startPos: any;
        endPos: any;
        constructorName: any;
        boundVariables: any;
    }): string {
        switch (pattern.tag) {
            case N_PatternWildcard:
            case N_PatternVariable:
                return undefined;
            case N_PatternNumber:
                return toStr(i18n('TYPE:Integer'));
            case N_PatternStructure:
                return this._symtable.constructorType(pattern.constructorName.value);
            case N_PatternTuple:
                return '_TUPLE_' + pattern.boundVariables.length.toString();
            case N_PatternTimeout:
                return toStr(i18n('TYPE:Event'));
            default:
                throw Error(
                    'Linter: pattern "' + Symbol.keyFor(pattern.tag) + '" not implemented.'
                );
        }
    }

    public _lintStmtAssignVariable(statement: { tag?: symbol; variable?: any; value?: any }): void {
        this._symtable.setLocalName(statement.variable, LocalVariable);
        this._lintExpression(statement.value);
    }

    public _lintStmtAssignTuple(statement: { tag?: symbol; variables?: any; value?: any }): void {
        const variables = {};
        for (const variable of statement.variables) {
            this._symtable.setLocalName(variable, LocalVariable);
            if (variable.value in variables) {
                this._lintCheck(
                    variable.startPos,
                    variable.endPos,
                    'repeated-variable-in-tuple-assignment',
                    [variable.value]
                );
            }
            variables[variable.value] = true;
        }
        this._lintExpression(statement.value);
    }

    public _lintStmtProcedureCall(statement: {
        tag?: symbol;
        procedureName?: any;
        startPos?: any;
        endPos?: any;
        args?: any;
    }): void {
        const name = statement.procedureName.value;

        /* Check that it is a procedure */
        if (!this._symtable.isProcedure(name)) {
            if (this._symtable.isConstructor(name)) {
                this._lintCheck(
                    statement.startPos,
                    statement.endPos,
                    'constructor-used-as-procedure',
                    [name, this._symtable.constructorType(name)]
                );
            } else {
                this._lintCheck(statement.startPos, statement.endPos, 'undefined-procedure', [
                    name
                ]);
            }
        }

        /* Check that the number of argument coincides */
        const expected = this._symtable.procedureParameters(name).length;
        const received = statement.args.length;
        if (expected !== received) {
            this._lintCheck(statement.startPos, statement.endPos, 'procedure-arity-mismatch', [
                name,
                expected,
                received
            ]);
        }

        /* Check all the arguments */
        for (const argument of statement.args) {
            this._lintExpression(argument);
        }
    }

    /** Patterns **/

    public _lintPattern(pattern: {
        tag?: any;
        startPos: any;
        endPos: any;
        constructorName: any;
        boundVariables: any;
    }): void {
        switch (pattern.tag) {
            case N_PatternWildcard:
                return this._lintPatternWildcard();
            case N_PatternVariable:
                return this._lintPatternVariable();
            case N_PatternNumber:
                return this._lintPatternNumber();
            case N_PatternStructure:
                return this._lintPatternStructure(pattern);
            case N_PatternTuple:
                return this._lintPatternTuple();
            case N_PatternTimeout:
                return this._lintPatternTimeout();
            default:
                throw Error(
                    'Linter: pattern "' + Symbol.keyFor(pattern.tag) + '" not implemented.'
                );
        }
    }

    public _lintPatternWildcard(): void {
        /* No restrictions */
    }

    public _lintPatternVariable(): void {
        /* No restrictions */
    }

    public _lintPatternNumber(): void {
        /* No restrictions */
    }

    public _lintPatternStructure(pattern: {
        tag?: any;
        startPos: any;
        endPos: any;
        constructorName: any;
        boundVariables: any;
    }): void {
        const name = pattern.constructorName.value;

        /* Check that the constructor exists */
        if (!this._symtable.isConstructor(name)) {
            this._failExpectedConstructorButGot(
                // throws
                pattern.startPos,
                pattern.endPos,
                name
            );
            return;
        }

        /* Check that the number of parameters match.
         * Note: constructor patterns with 0 arguments are always allowed.
         */
        const expected = this._symtable.constructorFields(name).length;
        const received = pattern.boundVariables.length;
        if (received > 0 && expected !== received) {
            this._lintCheck(pattern.startPos, pattern.endPos, 'structure-pattern-arity-mismatch', [
                name,
                expected,
                received
            ]);
        }
    }

    public _lintPatternTuple(): void {
        /* No restrictions */
    }

    public _lintPatternTimeout(): void {
        /* No restrictions */
    }

    /** Expressions **/

    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
    public _lintExpression(expression: any): void {
        switch (expression.tag) {
            case N_ExprVariable:
                return this._lintExprVariable();
            case N_ExprConstantNumber:
                return this._lintExprConstantNumber();
            case N_ExprConstantString:
                return this._lintExprConstantString();
            case N_ExprChoose:
                return this._lintExprChoose(expression);
            case N_ExprMatching:
                return this._lintExprMatching(expression);
            case N_ExprList:
                return this._lintExprList(expression);
            case N_ExprRange:
                return this._lintExprRange(expression);
            case N_ExprTuple:
                return this._lintExprTuple(expression);
            case N_ExprStructure:
                return this._lintExprStructure(expression);
            case N_ExprStructureUpdate:
                return this._lintExprStructureUpdate(expression);
            case N_ExprFunctionCall:
                return this._lintExprFunctionCall(expression);
            default:
                throw Error('Linter: Expression not implemented: ' + Symbol.keyFor(expression.tag));
        }
    }

    public _lintExprVariable(): void {
        /* No restrictions.
         * Note: the restriction that a variable is defined before it is used
         * is a dynamic constraint . */
    }

    public _lintExprConstantNumber(): void {
        /* No restrictions */
    }

    public _lintExprConstantString(): void {
        /* No restrictions */
    }

    public _lintExprChoose(expression: { condition: any; trueExpr: any; falseExpr: any }): void {
        this._lintExpression(expression.condition);
        this._lintExpression(expression.trueExpr);
        this._lintExpression(expression.falseExpr);
    }

    public _lintExprMatching(expression: { subject: any; branches: any }): void {
        this._lintExpression(expression.subject);
        this._lintMatchingBranches(expression.branches);
    }

    public _lintMatchingBranches(branches: any[]): void {
        this._lintBranches(branches, false /* !isInteractiveProgram */, true /* isMatching */);
    }

    public _lintExprList(expression: { elements: any }): void {
        for (const element of expression.elements) {
            this._lintExpression(element);
        }
    }

    public _lintExprRange(expression: { first: any; second: any; last: any }): void {
        this._lintExpression(expression.first);
        if (expression.second !== undefined) {
            this._lintExpression(expression.second);
        }
        this._lintExpression(expression.last);
    }

    public _lintExprTuple(expression: { elements: any }): void {
        for (const element of expression.elements) {
            this._lintExpression(element);
        }
    }

    public _lintExprStructure(expression: {
        tag?: symbol;
        constructorName?: any;
        startPos: any;
        endPos: any;
        fieldBindings?: any;
        original?: any;
        fieldNames?: (() => any) | (() => any) | (() => any);
    }): void {
        this._lintExprStructureOrUpdate(expression, undefined);
    }

    public _lintExprStructureUpdate(expression: {
        tag?: symbol;
        constructorName?: any;
        startPos: any;
        endPos: any;
        fieldBindings?: any;
        original?: any;
        fieldNames?: (() => any) | (() => any) | (() => any);
    }): void {
        this._lintExprStructureOrUpdate(expression, expression.original);
    }

    /* Check a structure construction: C(x1 <- e1, ..., xN <- eN)
     * or a structure update: C(original | x1 <- e1, ..., xN <- eN).
     *
     * If original is undefined, it is a structure construction.
     * If original is not undefined, it is the updated expression.
     * */
    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
    public _lintExprStructureOrUpdate(expression: any, original: { tag: symbol }): void {
        /* Check that constructor exists */
        const constructorName: string = expression.constructorName.value;
        if (!this._symtable.isConstructor(constructorName)) {
            this._failExpectedConstructorButGot(
                // throws
                expression.startPos,
                expression.endPos,
                constructorName
            );
            return;
        }

        this._checkStructureTypeNotEvent(constructorName, expression);
        this._checkStructureNoRepeatedFields(constructorName, expression);
        this._checkStructureBindingsCorrect(constructorName, expression);

        /* If it is a structure construction, check that the fields are complete */
        if (original === undefined) {
            this._checkStructureBindingsComplete(constructorName, expression);
        }

        /* If it is an update, recursively check the original expression */
        if (original !== undefined) {
            this._lintExpression(original);
        }

        /* Recursively check expressions in field bindings */
        for (const fieldBinding of expression.fieldBindings) {
            this._lintExpression(fieldBinding.value);
        }
    }

    /* Check that there are no repeated fields in a structure
     * construction/update */
    public _checkStructureNoRepeatedFields(
        constructorName: string,
        expression: { fieldNames: () => any; startPos: SourceReader; endPos: SourceReader }
    ): void {
        const declaredFields = expression.fieldNames();
        const seen = {};
        for (const fieldName of declaredFields) {
            if (fieldName in seen) {
                this._lintCheck(
                    expression.startPos,
                    expression.endPos,
                    'structure-construction-repeated-field',
                    [constructorName, fieldName]
                );
            }
            seen[fieldName] = true;
        }
    }

    /* Check that all bindings in a structure construction/update
     * correspond to existing fields */
    public _checkStructureBindingsCorrect(
        constructorName: string,
        expression: { fieldNames: () => any; startPos: SourceReader; endPos: SourceReader }
    ): void {
        const declaredFields = expression.fieldNames();
        const constructorFields = this._symtable.constructorFields(constructorName);
        for (const fieldName of declaredFields) {
            if (constructorFields.indexOf(fieldName) === -1) {
                this._lintCheck(
                    expression.startPos,
                    expression.endPos,
                    'structure-construction-invalid-field',
                    [constructorName, fieldName]
                );
            }
        }
    }

    /* Check that bindings in a structure construction/update
     * cover all existing fields */
    public _checkStructureBindingsComplete(
        constructorName: string,
        expression: { fieldNames: () => any; startPos: SourceReader; endPos: SourceReader }
    ): void {
        const declaredFields = expression.fieldNames();
        const constructorFields = this._symtable.constructorFields(constructorName);
        for (const fieldName of constructorFields) {
            if (declaredFields.indexOf(fieldName) === -1) {
                this._lintCheck(
                    expression.startPos,
                    expression.endPos,
                    'structure-construction-missing-field',
                    [constructorName, fieldName]
                );
            }
        }
    }

    /* Check that a structure construction/update does not involve
     * constructors of the Event type, which should only be
     * handled implicitly in an interactive program. */
    public _checkStructureTypeNotEvent(
        constructorName: string,
        expression: { startPos: SourceReader; endPos: SourceReader }
    ): void {
        const constructorType = this._symtable.constructorType(constructorName);
        if (constructorType === i18n('TYPE:Event')) {
            this._lintCheck(
                expression.startPos,
                expression.endPos,
                'structure-construction-cannot-be-an-event',
                [constructorName]
            );
        }
    }

    public _lintExprFunctionCall(expression: {
        functionName: { value: any };
        startPos: SourceReader;
        endPos: SourceReader;
        args: string | any[];
    }): void {
        /* Check that it is a function or a field */
        const name = expression.functionName.value;
        if (!this._symtable.isFunction(name) && !this._symtable.isField(name)) {
            this._lintCheck(expression.startPos, expression.endPos, 'undefined-function', [name]);
        }

        /* Check that the number of argument coincides */
        let expected: number;
        if (this._symtable.isFunction(name)) {
            expected = this._symtable.functionParameters(name).length;
        } else {
            /* Fields always have exactly one parameter */
            expected = 1;
        }
        const received = expression.args.length;
        if (expected !== received) {
            this._lintCheck(expression.startPos, expression.endPos, 'function-arity-mismatch', [
                name,
                expected,
                received
            ]);
        }

        /* Recursively check arguments */
        for (const argument of expression.args) {
            this._lintExpression(argument);
        }
    }

    public _disableRecursion(ast: ASTMain): void {
        if (this._enabledLinterChecks['forbidden-extension-allow-recursion']) {
            const cycle = new RecursionChecker().callCycle(ast);
            if (cycle !== undefined) {
                this._lintCheck(
                    cycle[0].location.startPos,
                    cycle[0].location.endPos,
                    'forbidden-extension-allow-recursion',
                    [cycle]
                );
            }
        }
    }

    /* Throw a syntax error indicating that we expected the name of a
     * constructor, but we got a name which is not a constructor.
     *
     * If the name is a type or a procedure, provide a more helpful
     * error message. (Coinciding constructor and procedure names are
     * not forbidden, but it is probably a mistake). */
    public _failExpectedConstructorButGot(
        startPos: SourceReader,
        endPos: SourceReader,
        name: string
    ): void {
        if (this._symtable.isType(name)) {
            this._lintCheck(startPos, endPos, 'type-used-as-constructor', [
                name,
                this._symtable.typeConstructors(name)
            ]);
        } else if (this._symtable.isProcedure(name)) {
            this._lintCheck(startPos, endPos, 'procedure-used-as-constructor', [name]);
        } else {
            this._lintCheck(startPos, endPos, 'undeclared-constructor', [name]);
        }
    }
}
