import * as ESTree from 'estree';

import { TNodeWithStatements } from '../types/node/TNodeWithStatements';
import { TStatement } from '../types/node/TStatement';

import { IStackTraceData } from '../interfaces/analyzers/stack-trace-analyzer/IStackTraceData';

import { NodeGuards } from './NodeGuards';

export class NodeAppender {
    /**
     * @param {TNodeWithStatements} nodeWithStatements
     * @param {TStatement[]} statements
     */
    public static append (nodeWithStatements: TNodeWithStatements, statements: TStatement[]): void {
        statements = NodeAppender.parentizeScopeStatementsBeforeAppend(nodeWithStatements, statements);

        NodeAppender.setScopeStatements(nodeWithStatements, [
            ...NodeAppender.getScopeStatements(nodeWithStatements),
            ...statements
        ]);
    }

    /**
     * Appends node into a first deepest BlockStatement in order of function calls
     *
     * For example:
     *
     * function Foo () {
     *     var baz = function () {
     *
     *     }
     *
     *     baz();
     * }
     *
     * foo();
     *
     * Appends node into block statement of `baz` function expression
     *
     * @param {IStackTraceData[]} stackTraceData
     * @param {TNodeWithStatements} nodeWithStatements
     * @param {TStatement[]} bodyStatements
     * @param {number} index
     */
    public static appendToOptimalBlockScope (
        stackTraceData: IStackTraceData[],
        nodeWithStatements: TNodeWithStatements,
        bodyStatements: TStatement[],
        index: number = 0
    ): void {
        const targetBlockScope: TNodeWithStatements = stackTraceData.length
            ? NodeAppender.getOptimalBlockScope(stackTraceData, index)
            : nodeWithStatements;

        NodeAppender.prepend(targetBlockScope, bodyStatements);
    }

    /**
     * Returns deepest block scope node at given deep.
     *
     * @param {IStackTraceData[]} stackTraceData
     * @param {number} index
     * @param {number} deep
     * @returns {BlockStatement}
     */
    public static getOptimalBlockScope (
        stackTraceData: IStackTraceData[],
        index: number,
        deep: number = Infinity
    ): ESTree.BlockStatement {
        const firstCall: IStackTraceData = stackTraceData[index];

        if (deep <= 0) {
            throw new Error('Invalid `deep` argument value. Value should be bigger then 0.');
        }

        if (deep > 1 && firstCall.stackTrace.length) {
            return NodeAppender.getOptimalBlockScope(firstCall.stackTrace, 0, --deep);
        } else {
            return firstCall.callee;
        }
    }

    /**
     * @param {TNodeWithStatements} nodeWithStatements
     * @param {TStatement[]} statements
     * @param {Node} target
     */
    public static insertAfter (
        nodeWithStatements: TNodeWithStatements,
        statements: TStatement[],
        target: ESTree.Statement
    ): void {
        const indexInScopeStatement: number = NodeAppender
            .getScopeStatements(nodeWithStatements)
            .indexOf(target);

        NodeAppender.insertAtIndex(nodeWithStatements, statements, indexInScopeStatement + 1);
    }

    /**
     * @param {TNodeWithStatements} nodeWithStatements
     * @param {TStatement[]} statements
     * @param {number} index
     */
    public static insertAtIndex (
        nodeWithStatements: TNodeWithStatements,
        statements: TStatement[],
        index: number
    ): void {
        statements = NodeAppender.parentizeScopeStatementsBeforeAppend(nodeWithStatements, statements);

        NodeAppender.setScopeStatements(nodeWithStatements, [
            ...NodeAppender.getScopeStatements(nodeWithStatements).slice(0, index),
            ...statements,
            ...NodeAppender.getScopeStatements(nodeWithStatements).slice(index)
        ]);
    }

    /**
     * @param {TNodeWithStatements} nodeWithStatements
     * @param {TStatement[]} statements
     */
    public static prepend (nodeWithStatements: TNodeWithStatements, statements: TStatement[]): void {
        statements = NodeAppender.parentizeScopeStatementsBeforeAppend(nodeWithStatements, statements);

        NodeAppender.setScopeStatements(nodeWithStatements, [
            ...statements,
            ...NodeAppender.getScopeStatements(nodeWithStatements),
        ]);
    }

    /**
     * @param {TNodeWithStatements} nodeWithStatements
     * @returns {TStatement[]}
     */
    private static getScopeStatements (nodeWithStatements: TNodeWithStatements): TStatement[] {
        if (NodeGuards.isSwitchCaseNode(nodeWithStatements)) {
            return nodeWithStatements.consequent;
        }

        return nodeWithStatements.body;
    }

    /**
     * @param {TNodeWithStatements} nodeWithStatements
     * @param {TStatement[]} statements
     * @returns {TStatement[]}
     */
    private static parentizeScopeStatementsBeforeAppend (
        nodeWithStatements: TNodeWithStatements,
        statements: TStatement[]
    ): TStatement[] {
        statements.forEach((statement: TStatement) => {
            statement.parentNode = nodeWithStatements;
        });

        return statements;
    }

    /**
     * @param {TNodeWithStatements} nodeWithStatements
     * @param {TStatement[]} statements
     */
    private static setScopeStatements (nodeWithStatements: TNodeWithStatements, statements: TStatement[]): void {
        if (NodeGuards.isSwitchCaseNode(nodeWithStatements)) {
            nodeWithStatements.consequent = <ESTree.Statement[]>statements;

            return;
        }

        nodeWithStatements.body = statements;
    }
}
