/* eslint-disable camelcase */
/* eslint-disable no-underscore-dangle */
import {
    N_DefFunction,
    N_DefInteractiveProgram,
    N_DefProcedure,
    N_DefProgram,
    N_ExprFunctionCall,
    N_StmtProcedureCall
} from './ast';

import { SourceReader } from './reader';
import { Token } from './token';

export class RecursionChecker {
    /*
     * Each routine call (i.e. procedure or function call) in the source
     * code is of the form:
     *    R(e1, ..., en)
     * where R is the identifier for the routine.
     *
     * The token R is called the 'location' of the call.
     * Observe that the location includes not only the name of the
     * routine but also its position in the source code.
     *
     * The call graph is a dictionary whose keys are strings
     * and whose values are again dictionaries.
     * The outer and the inner dictionaries are indexed by routine names
     * in such a way that:
     *
     *   _callGraph[F][G]
     *
     * is the location of the first call to G inside the body of F.
     */
    private _currentRoutine: any;
    private _callGraph: Record<string, Record<string, { value: string }>> = {};

    /*
     * If there is a cycle in the call graph (using either procedure calls
     * or function calls), return a list:
     *   [c1, ..., cn]
     * where ci is the i-th call involved in a cycle.
     * A call is of the form:
     *   {caller: F , callee: G, location: L}
     * where F is the name (string) of the caller,
     *       G is the name (string) of the callee,
     *   and L is the location of the call.
     *
     * Otherwise return null.
     */
    public callCycle(ast: {
        definitions: any[];
        startPos: SourceReader;
        endPos: SourceReader;
    }): any {
        /* Build the call graph */
        this._visitNode(ast);

        /* Find a cycle in the call graph */
        return this._findCallCycle();
    }

    /* Visitor -- build the call graph */

    public _addEdge(caller: string, callee: { value: string }): void {
        if (!(caller in this._callGraph)) {
            this._callGraph[caller] = {};
        }
        if (!(callee.value in this._callGraph[caller])) {
            this._callGraph[caller][callee.value] = callee;
        }
    }

    public _visitNode(node: {
        definitions: any[];
        startPos: SourceReader;
        endPos: SourceReader;
    }): void {
        if (node === undefined || node instanceof Token) {
            /* Skip */
        } else if (node instanceof Array) {
            this._visitNodes(node);
        } else {
            this._visitTaggedNode(node);
        }
    }

    public _visitNodes(
        nodes: {
            definitions: any[];
            startPos: SourceReader;
            endPos: SourceReader;
        }[]
    ): void {
        for (const node of nodes) {
            this._visitNode(node);
        }
    }

    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
    public _visitTaggedNode(node: any): void {
        switch (node.tag) {
            case N_DefProgram:
            case N_DefInteractiveProgram:
                this._visitProgramDefinition();
                break;
            case N_DefProcedure:
            case N_DefFunction:
                this._visitRoutineDefinition(node);
                break;
            case N_StmtProcedureCall:
                this._visitProcedureCall(node);
                break;
            case N_ExprFunctionCall:
                this._visitFunctionCall(node);
                break;
            default:
                break;
        }
        this._visitNodes(node.children);
    }

    public _visitProgramDefinition(): void {
        this._currentRoutine = 'program';
    }

    public _visitRoutineDefinition(node: { name: { value: any } }): void {
        this._currentRoutine = node.name.value;
    }

    public _visitProcedureCall(node: { procedureName: { value: string } }): void {
        this._addEdge(this._currentRoutine, node.procedureName);
    }

    public _visitFunctionCall(node: { functionName: { value: string } }): void {
        this._addEdge(this._currentRoutine, node.functionName);
    }

    /* Find a cycle in the call graph */

    public _findCallCycle():
        | { caller: any; callee: string; location: { value: string } }[]
        | { caller: string }[] {
        const visited = {};
        const parents = {};
        for (const f in this._callGraph) {
            visited[f] = true;
            parents[f] = true;
            const cycle = this._findCallCycleFrom(visited, parents, [], f);
            if (cycle !== undefined) {
                return cycle;
            }
            delete parents[f];
        }
        return undefined;
    }

    public _findCallCycleFrom(
        visited: { [x: string]: boolean },
        parents: { [x: string]: any },
        path: { caller: any; callee: string; location: { value: string } }[] | { caller: string }[],
        f: string
    ): { caller: any; callee: string; location: { value: string } }[] | { caller: string }[] {
        for (const g in this._callGraph[f]) {
            path.push({
                caller: f,
                callee: g,
                location: this._callGraph[f][g]
            });
            if (g in parents) {
                while (path[0].caller !== g) {
                    path.shift();
                }
                path.push();
                return path; /* Cycle */
            }
            if (!(g in visited)) {
                visited[g] = true;
                parents[g] = true;
                const cycle = this._findCallCycleFrom(visited, parents, path, g);
                if (cycle !== undefined) {
                    return cycle;
                }
                delete parents[g];
            }
            path.pop();
        }
        return undefined;
    }
}
