import { injectable, inject } from 'inversify';
import { ServiceIdentifiers } from '../container/ServiceIdentifiers';

import * as estraverse from 'estraverse';
import * as ESTree from 'estree';

import { TCalleeDataExtractorFactory } from '../types/container/TCalleeDataExtractorFactory';

import { ICalleeData } from '../interfaces/stack-trace-analyzer/ICalleeData';
import { ICalleeDataExtractor } from '../interfaces/stack-trace-analyzer/ICalleeDataExtractor';
import { IStackTraceAnalyzer } from '../interfaces/stack-trace-analyzer/IStackTraceAnalyzer';
import { IStackTraceData } from '../interfaces/stack-trace-analyzer/IStackTraceData';

import { CalleeDataExtractors } from '../enums/container/CalleeDataExtractors';

import { Node } from '../node/Node';
import { NodeUtils } from '../node/NodeUtils';

/**
 * This class generates a data with code stack trace functions calls
 *
 * For example:
 *
 * function Foo () {
 *     var baz = function () {
 *
 *     }
 *
 *     baz();
 * }
 *
 * foo();
 *
 * Will generate a structure like:
 *
 * [
 *      {
 *          callee: FOO_FUNCTION_NODE
 *          name: 'Foo',
 *          trace: [
 *              {
 *                  callee: BAZ_FUNCTION_NODE,
 *                  name: 'baz,
 *                  trace: []
 *              }
 *          ]
 *      }
 * ]
 */
@injectable()
export class StackTraceAnalyzer implements IStackTraceAnalyzer {
    /**
     * @type {CalleeDataExtractors[]}
     */
    private static readonly calleeDataExtractorsList: CalleeDataExtractors[] = [
        CalleeDataExtractors.FunctionDeclarationCalleeDataExtractor,
        CalleeDataExtractors.FunctionExpressionCalleeDataExtractor,
        CalleeDataExtractors.ObjectExpressionCalleeDataExtractor
    ];

    /**
     * @type {number}
     */
    private static readonly limitThresholdActivationLength: number = 25;

    /**
     * @type {number}
     */
    private static readonly limitThreshold: number = 0.002;

    /**
     * @type {(calleeDataExtractorName: CalleeDataExtractors) => ICalleeDataExtractor}
     */
    private calleeDataExtractorFactory: (calleeDataExtractorName: CalleeDataExtractors) => ICalleeDataExtractor;

    constructor (
        @inject(ServiceIdentifiers.Factory__ICalleeDataExtractor) calleeDataExtractorFactory: TCalleeDataExtractorFactory
    ) {
        this.calleeDataExtractorFactory = calleeDataExtractorFactory;
    }

    /**
     * @param blockScopeBodyLength
     * @returns {number}
     */
    public static getLimitIndex (blockScopeBodyLength: number): number {
        const lastIndex: number = blockScopeBodyLength - 1;
        const limitThresholdActivationIndex: number = StackTraceAnalyzer.limitThresholdActivationLength - 1;

        let limitIndex: number = lastIndex;

        if (lastIndex > limitThresholdActivationIndex) {
            limitIndex = Math.round(
                limitThresholdActivationIndex + (lastIndex * StackTraceAnalyzer.limitThreshold)
            );

            if (limitIndex > lastIndex) {
                limitIndex = lastIndex;
            }
        }

        return limitIndex;
    }

    /**
     * @param blockScopeBody
     * @returns {IStackTraceData[]}
     */
    public analyze (blockScopeBody: ESTree.Node[]): IStackTraceData[] {
        return this.analyzeRecursive(blockScopeBody);
    }

    /**
     * @param blockScopeBody
     * @returns {IStackTraceData[]}
     */
    private analyzeRecursive (blockScopeBody: ESTree.Node[]): IStackTraceData[] {
        const limitIndex: number = StackTraceAnalyzer.getLimitIndex(blockScopeBody.length);
        const stackTraceData: IStackTraceData[] = [];
        const blockScopeBodyLength: number = blockScopeBody.length;

        for (let index: number = 0; index < blockScopeBodyLength; index++) {
            if (index > limitIndex) {
                break;
            }

            const blockScopeBodyNode: ESTree.Node = blockScopeBody[index];

            estraverse.traverse(blockScopeBodyNode, {
                enter: (node: ESTree.Node): any => {
                    if (!Node.isCallExpressionNode(node)) {
                        return;
                    }

                    if (blockScopeBodyNode.parentNode !== NodeUtils.getBlockScopesOfNode(node)[0]) {
                        return estraverse.VisitorOption.Skip;
                    }

                    this.analyzeCallExpressionNode(stackTraceData, blockScopeBody, node);
                }
            });
        }

        return stackTraceData;
    }

    /**
     * @param stackTraceData
     * @param blockScopeBody
     * @param callExpressionNode
     * @returns {IStackTraceData[]}
     */
    private analyzeCallExpressionNode (
        stackTraceData: IStackTraceData[],
        blockScopeBody: ESTree.Node[],
        callExpressionNode: ESTree.CallExpression
    ): void {
        StackTraceAnalyzer.calleeDataExtractorsList.forEach((calleeDataExtractorName: CalleeDataExtractors) => {
            const calleeData: ICalleeData | null = this.calleeDataExtractorFactory(calleeDataExtractorName)
                .extract(blockScopeBody, callExpressionNode.callee);

            if (!calleeData) {
                return;
            }

            stackTraceData.push({
                ...calleeData,
                stackTrace: this.analyzeRecursive(calleeData.callee.body)
            });
        });
    }
}
