/* eslint-disable camelcase */
/* eslint-disable no-underscore-dangle */
import { SourceReader, UnknownPosition } from './reader';

import { Token } from './token';

export const N_Main = Symbol.for('N_Main');
/* Definitions */
export const N_DefProgram = Symbol.for('N_DefProgram');
export const N_DefInteractiveProgram = Symbol.for('N_DefInteractiveProgram');
export const N_DefProcedure = Symbol.for('N_DefProcedure');
export const N_DefFunction = Symbol.for('N_DefFunction');
export const N_DefType = Symbol.for('N_DefType');
/* Statements */
export const N_StmtBlock = Symbol.for('N_StmtBlock');
export const N_StmtReturn = Symbol.for('N_StmtReturn');
export const N_StmtIf = Symbol.for('N_StmtIf');
export const N_StmtRepeat = Symbol.for('N_StmtRepeat');
export const N_StmtForeach = Symbol.for('N_StmtForeach');
export const N_StmtWhile = Symbol.for('N_StmtWhile');
export const N_StmtSwitch = Symbol.for('N_StmtSwitch');
export const N_StmtAssignVariable = Symbol.for('N_StmtAssignVariable');
export const N_StmtAssignTuple = Symbol.for('N_StmtAssignTuple');
export const N_StmtProcedureCall = Symbol.for('N_StmtProcedureCall');
/* Patterns */
export const N_PatternWildcard = Symbol.for('N_PatternWildcard');
export const N_PatternVariable = Symbol.for('N_PatternVariable');
export const N_PatternNumber = Symbol.for('N_PatternNumber');
export const N_PatternStructure = Symbol.for('N_PatternStructure');
export const N_PatternTuple = Symbol.for('N_PatternTuple');
export const N_PatternTimeout = Symbol.for('N_PatternTimeout');
/* Expressions */
export const N_ExprVariable = Symbol.for('N_ExprVariable');
export const N_ExprConstantNumber = Symbol.for('N_ExprConstantNumber');
export const N_ExprConstantString = Symbol.for('N_ExprConstantString');
export const N_ExprChoose = Symbol.for('N_ExprChoose');
export const N_ExprMatching = Symbol.for('N_ExprMatching');
export const N_ExprList = Symbol.for('N_ExprList');
export const N_ExprRange = Symbol.for('N_ExprRange');
export const N_ExprTuple = Symbol.for('N_ExprTuple');
export const N_ExprStructure = Symbol.for('N_ExprStructure');
export const N_ExprStructureUpdate = Symbol.for('N_ExprStructureUpdate');
export const N_ExprFunctionCall = Symbol.for('N_ExprFunctionCall');
/* SwitchBranch: pattern -> body */
export const N_SwitchBranch = Symbol.for('N_SwitchBranch');
/* MatchingBranch: pattern -> body */
export const N_MatchingBranch = Symbol.for('N_MatchingBranch');
/* FieldBinding: fieldName <- value */
export const N_FieldBinding = Symbol.for('N_FieldBinding');
/* ConstructorDeclaration */
export const N_ConstructorDeclaration = Symbol.for('N_ConstructorDeclaration');

/* Helper functions for the ASTNode toString method */

function indent(string: string): string {
    const lines = [];
    for (const line of string.split('\n')) {
        lines.push('  ' + line);
    }
    return lines.join('\n');
}

function showAST(node: ASTNode | Token | ASTNode[]): string {
    if (node === undefined) {
        return 'null';
    } else if (node instanceof Array) {
        return '[\n' + showASTs(node).join(',\n') + '\n]';
    } else if (node instanceof Token) {
        return node.toString();
    }
    const tag = Symbol.keyFor(node.tag).substring(2);
    return tag + '(\n' + showASTs(node.children).join(',\n') + '\n)';
}

function showASTs(nodes: ASTNode[]): string[] {
    const res = [];
    for (const node of nodes) {
        res.push(indent(showAST(node)));
    }
    return res;
}

export type ASTDef = ASTDefType | ASTDefProgram | ASTDefInteractiveProgram | ASTDefFunction;

/** An instance of ASTNode represents a node of the abstract syntax tree.
 * - tag should be a node tag symbol.
 * - children should be (recursively) a possibly empty array of ASTNode's.
 * - startPos and endPos represent the starting and ending
 *   position of the code fragment in the source code, to aid error
 *   reporting.
 **/
export class ASTNode {
    private _tag: symbol;
    private _children: any[];
    private _startPos: SourceReader;
    private _endPos: SourceReader;
    private _attributes: Record<string, ASTDef>;

    public constructor(tag: symbol, children: any[]) {
        this._tag = tag;
        this._children = children;
        this._startPos = UnknownPosition;
        this._endPos = UnknownPosition;
        this._attributes = {};

        /* Assert this invariant to protect against common mistakes. */
        if (!(children instanceof Array)) {
            throw Error('The children of an ASTNode should be an array.');
        }
    }

    public toMulangLike(): any {
        return {
            tag: this._tag.toString().replace(/(^Symbol\(|\)$)/g, ''),
            contents: this._children.map((node) => {
                if (node === undefined) {
                    return 'null';
                } else if (node instanceof Array) {
                    return new ASTNode(Symbol('?'), node).toMulangLike().contents;
                } else if (node instanceof ASTNode) {
                    return node.toMulangLike();
                } else if (node instanceof Token) {
                    return node.toString();
                } else {
                    return '?';
                }
            })
        };
    }

    public toString(): string {
        return showAST(this);
    }

    public get tag(): symbol {
        return this._tag;
    }

    public get children(): any[] {
        return this._children;
    }

    public set startPos(position: SourceReader) {
        this._startPos = position;
    }

    public get startPos(): SourceReader {
        return this._startPos;
    }

    public set endPos(position: SourceReader) {
        this._endPos = position;
    }

    public get endPos(): SourceReader {
        return this._endPos;
    }

    public get attributes(): Record<string, ASTDef> {
        return this._attributes;
    }

    public set attributes(attributes: Record<string, ASTDef>) {
        this._attributes = attributes;
    }
}

/* Main */

export class ASTMain extends ASTNode {
    public constructor(definitions: ASTDef[]) {
        super(N_Main, definitions);
    }

    public get definitions(): ASTDef[] {
        return this.children;
    }
}

/* Definitions */

export class ASTDefProgram extends ASTNode {
    public constructor(body: ASTStmtBlock) {
        super(N_DefProgram, [body]);
    }

    public get body(): ASTStmtBlock {
        return this.children[0];
    }
}

export abstract class ASTNodeWithBranches extends ASTNode {
    public get branches(): ASTNodeWithPattern[] {
        return this.children;
    }
}

export class ASTDefInteractiveProgram extends ASTNodeWithBranches {
    public constructor(branches: ASTNode[]) {
        super(N_DefInteractiveProgram, branches);
    }

    public get branches(): ASTNodeWithPattern[] {
        return this.children;
    }
}

export class ASTDefProcedure extends ASTNode {
    public constructor(name: Token, parameters: Token[], body: ASTNode) {
        super(N_DefProcedure, [name, parameters, body]);
    }

    public get name(): Token {
        return this.children[0];
    }

    public get parameters(): Token[] {
        return this.children[1];
    }

    public get body(): ASTNode {
        return this.children[2];
    }
}

export class ASTDefFunction extends ASTNode {
    public constructor(name: Token, parameters: Token[], body: ASTNode) {
        super(N_DefFunction, [name, parameters, body]);
    }

    public get name(): Token {
        return this.children[0];
    }

    public get parameters(): Token[] {
        return this.children[1];
    }

    public get body(): ASTNode {
        return this.children[2];
    }
}

export class ASTDefType extends ASTNode {
    public constructor(typeName: Token, constructorDeclarations: ASTConstructorDeclaration[]) {
        super(N_DefType, [typeName, constructorDeclarations]);
    }

    public get typeName(): Token {
        return this.children[0];
    }

    public get constructorDeclarations(): ASTConstructorDeclaration[] {
        return this.children[1];
    }
}

/* Statements */

export class ASTStmtBlock extends ASTNode {
    public constructor(statements: ASTNode[]) {
        super(N_StmtBlock, statements);
    }

    public get statements(): ASTNode[] {
        return this.children;
    }
}

export class ASTStmtReturn extends ASTNode {
    public constructor(result: ASTExpr) {
        super(N_StmtReturn, [result]);
    }

    public get result(): ASTExpr {
        return this.children[0];
    }
}

export class ASTStmtIf extends ASTNode {
    // Note: elseBlock may be null
    public constructor(condition: ASTExpr, thenBlock: ASTNode, elseBlock: ASTNode) {
        super(N_StmtIf, [condition, thenBlock, elseBlock]);
    }

    public get condition(): ASTExpr {
        return this.children[0];
    }

    public get thenBlock(): ASTNode {
        return this.children[1];
    }

    public get elseBlock(): ASTNode {
        return this.children[2];
    }
}

export class ASTStmtRepeat extends ASTNode {
    public constructor(times: ASTExpr, body: ASTNode) {
        super(N_StmtRepeat, [times, body]);
    }

    public get times(): ASTExpr {
        return this.children[0];
    }

    public get body(): ASTNode {
        return this.children[1];
    }
}

export abstract class ASTNodeWithPattern extends ASTNode {
    public get pattern(): ASTPattern {
        return this.children[0];
    }
}

export class ASTStmtForeach extends ASTNodeWithPattern {
    public constructor(pattern: ASTNode, range: ASTExpr, body: ASTNode) {
        super(N_StmtForeach, [pattern, range, body]);
    }

    public get pattern(): ASTPattern {
        return this.children[0];
    }

    public get range(): ASTExpr {
        return this.children[1];
    }

    public get body(): ASTNode {
        return this.children[2];
    }
}

export class ASTStmtWhile extends ASTNode {
    public constructor(condition: ASTExpr, body: ASTNode) {
        super(N_StmtWhile, [condition, body]);
    }

    public get condition(): ASTExpr {
        return this.children[0];
    }

    public get body(): ASTNode {
        return this.children[1];
    }
}

export class ASTStmtSwitch extends ASTNodeWithBranches {
    public constructor(subject: ASTExpr, branches: ASTNodeWithPattern[]) {
        super(N_StmtSwitch, [subject, branches]);
    }

    public get subject(): ASTExpr {
        return this.children[0];
    }

    public get branches(): ASTNodeWithPattern[] {
        return this.children[1];
    }
}

export class ASTSwitchBranch extends ASTNodeWithPattern {
    public constructor(pattern: ASTPattern, body: ASTNode) {
        super(N_SwitchBranch, [pattern, body]);
    }

    public get pattern(): ASTPattern {
        return this.children[0];
    }

    public get body(): ASTNode {
        return this.children[1];
    }
}

export class ASTMatchingBranch extends ASTNodeWithPattern {
    public constructor(pattern: ASTPattern, body: ASTExpr) {
        super(N_MatchingBranch, [pattern, body]);
    }

    public get pattern(): ASTPattern {
        return this.children[0];
    }

    public get body(): ASTExpr {
        return this.children[1];
    }
}

export class ASTStmtAssignVariable extends ASTNode {
    public constructor(variable: Token, value: ASTExpr) {
        super(N_StmtAssignVariable, [variable, value]);
    }

    public get variable(): Token {
        return this.children[0];
    }

    public get value(): ASTExpr {
        return this.children[1];
    }
}

export class ASTStmtAssignTuple extends ASTNode {
    public constructor(variables: Token[], value: ASTExpr) {
        super(N_StmtAssignTuple, [variables, value]);
    }

    public get variables(): Token[] {
        return this.children[0];
    }

    public get value(): ASTExpr {
        return this.children[1];
    }
}

export class ASTStmtProcedureCall extends ASTNode {
    public constructor(procedureName: Token, args: ASTExpr[]) {
        super(N_StmtProcedureCall, [procedureName, args]);
    }

    public get procedureName(): Token {
        return this.children[0];
    }

    public get args(): ASTExpr[] {
        return this.children[1];
    }
}

/* Patterns */

export type ASTPattern =
    | ASTPatternWildcard
    | ASTPatternVariable
    | ASTPatternNumber
    | ASTPatternStructure
    | ASTPatternTuple
    | ASTPatternTimeout;

export class ASTPatternWildcard extends ASTNode {
    public constructor(statement?: ASTStmtBlock) {
        super(N_PatternWildcard, []);
    }

    public get boundVariables(): ASTNode[] {
        return [];
    }
}

export class ASTPatternVariable extends ASTNode {
    public constructor(variableName: Token) {
        super(N_PatternVariable, [variableName]);
    }

    public get variableName(): Token {
        return this.children[0];
    }

    public get boundVariables(): Token[] {
        return [this.children[0]];
    }
}

export class ASTPatternNumber extends ASTNode {
    public constructor(number: Token) {
        super(N_PatternNumber, [number]);
    }

    public get number(): Token {
        return this.children[0];
    }

    public get boundVariables(): ASTNode[] {
        return [];
    }
}

export class ASTPatternStructure extends ASTNode {
    public constructor(constructorName: Token, parameters: Token[]) {
        super(N_PatternStructure, [constructorName, parameters]);
    }

    public get constructorName(): Token {
        return this.children[0];
    }

    public get boundVariables(): Token[] {
        return this.children[1];
    }
}

export class ASTPatternTuple extends ASTNode {
    public constructor(parameters: Token[]) {
        super(N_PatternTuple, parameters);
    }

    public get boundVariables(): Token[] {
        return this.children;
    }
}

export class ASTPatternTimeout extends ASTNode {
    public constructor(timeout: Token) {
        super(N_PatternTimeout, [timeout]);
    }

    public get boundVariables(): ASTNode[] {
        return [];
    }

    public get timeout(): number {
        return parseInt(this.children[0].value, 10);
    }
}

/* Expressions */

export type ASTExpr =
    | ASTExprVariable
    | ASTExprConstantNumber
    | ASTExprConstantString
    | ASTExprChoose
    | ASTExprMatching
    | ASTExprList
    | ASTExprRange
    | ASTExprTuple
    | ASTExprStructure
    | ASTExprStructureUpdate
    | ASTExprFunctionCall;

export class ASTExprVariable extends ASTNode {
    public constructor(variableName: Token) {
        super(N_ExprVariable, [variableName]);
    }

    public get variableName(): Token {
        return this.children[0];
    }
}

export class ASTExprConstantNumber extends ASTNode {
    public constructor(number: Token) {
        super(N_ExprConstantNumber, [number]);
    }

    public get number(): Token {
        return this.children[0];
    }
}

export class ASTExprConstantString extends ASTNode {
    public constructor(string: Token) {
        super(N_ExprConstantString, [string]);
    }

    public get string(): Token {
        return this.children[0];
    }
}

export class ASTExprChoose extends ASTNode {
    public constructor(condition: ASTExpr, trueExpr: ASTExpr, falseExpr: ASTExpr) {
        super(N_ExprChoose, [condition, trueExpr, falseExpr]);
    }

    public get condition(): ASTExpr {
        return this.children[0];
    }

    public get trueExpr(): ASTExpr {
        return this.children[1];
    }

    public get falseExpr(): ASTExpr {
        return this.children[2];
    }
}

export class ASTExprMatching extends ASTNodeWithBranches {
    public constructor(subject: ASTExpr, branches: ASTNode[]) {
        super(N_ExprMatching, [subject, branches]);
    }

    public get subject(): ASTExpr {
        return this.children[0];
    }

    public get branches(): ASTNodeWithPattern[] {
        return this.children[1];
    }
}

export class ASTExprList extends ASTNode {
    public constructor(elements: ASTExpr[]) {
        super(N_ExprList, elements);
    }

    public get elements(): ASTExpr[] {
        return this.children;
    }
}

export class ASTExprRange extends ASTNode {
    // Note: second may be null
    public constructor(first: ASTExpr, second: ASTExpr, last: ASTExpr) {
        super(N_ExprRange, [first, second, last]);
    }

    public get first(): ASTExpr {
        return this.children[0];
    }

    public get second(): ASTExpr {
        return this.children[1];
    }

    public get last(): ASTExpr {
        return this.children[2];
    }
}

export class ASTExprTuple extends ASTNode {
    public constructor(elements: ASTExpr[]) {
        super(N_ExprTuple, elements);
    }

    public get elements(): ASTExpr[] {
        return this.children;
    }
}

export class ASTExprStructure extends ASTNode {
    public constructor(constructorName: Token, fieldBindings: ASTFieldBinding[]) {
        super(N_ExprStructure, [constructorName, fieldBindings]);
    }

    public get constructorName(): Token {
        return this.children[0];
    }

    public get fieldBindings(): ASTFieldBinding[] {
        return this.children[1];
    }

    public fieldNames(): ASTNode[] {
        const names = [];
        for (const fieldBinding of this.fieldBindings) {
            names.push(fieldBinding.fieldName.value);
        }
        return names;
    }
}

export class ASTExprStructureUpdate extends ASTNode {
    public constructor(
        constructorName: Token,
        original: ASTExpr,
        fieldBindings: ASTFieldBinding[]
    ) {
        super(N_ExprStructureUpdate, [constructorName, original, fieldBindings]);
    }

    public get constructorName(): Token {
        return this.children[0];
    }

    public get original(): ASTExpr {
        return this.children[1];
    }

    public get fieldBindings(): ASTFieldBinding[] {
        return this.children[2];
    }

    public fieldNames(): Token[] {
        const names = [];
        for (const fieldBinding of this.fieldBindings) {
            names.push(fieldBinding.fieldName.value);
        }
        return names;
    }
}

export class ASTExprFunctionCall extends ASTNode {
    public constructor(functionName: Token, args: ASTExpr[]) {
        super(N_ExprFunctionCall, [functionName, args]);
    }

    public get functionName(): Token {
        return this.children[0];
    }

    public get args(): ASTExpr[] {
        return this.children[1];
    }
}

export class ASTFieldBinding extends ASTNode {
    public constructor(fieldName: Token, value: ASTExpr) {
        super(N_FieldBinding, [fieldName, value]);
    }

    public get fieldName(): Token {
        return this.children[0];
    }

    public get value(): ASTExpr {
        return this.children[1];
    }
}

export class ASTConstructorDeclaration extends ASTNode {
    public constructor(constructorName: Token, fieldNames: Token[]) {
        super(N_ConstructorDeclaration, [constructorName, fieldNames]);
    }

    public get constructorName(): Token {
        return this.children[0];
    }

    public get fieldNames(): Token[] {
        return this.children[1];
    }
}
