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

import * as acorn from 'acorn';
import * as escodegen from 'escodegen';
import * as ESTree from 'estree';

import { TObfuscatedCodeFactory } from './types/container/source-code/TObfuscatedCodeFactory';

import { ICodeTransformersRunner } from './interfaces/code-transformers/ICodeTransformersRunner';
import { IGeneratorOutput } from './interfaces/IGeneratorOutput';
import { IJavaScriptObfuscator } from './interfaces/IJavaScriptObfsucator';
import { ILogger } from './interfaces/logger/ILogger';
import { IObfuscatedCode } from './interfaces/source-code/IObfuscatedCode';
import { IOptions } from './interfaces/options/IOptions';
import { IRandomGenerator } from './interfaces/utils/IRandomGenerator';
import { INodeTransformersRunner } from './interfaces/node-transformers/INodeTransformersRunner';

import { CodeTransformer } from './enums/code-transformers/CodeTransformer';
import { CodeTransformationStage } from './enums/code-transformers/CodeTransformationStage';
import { LoggingMessage } from './enums/logger/LoggingMessage';
import { NodeTransformer } from './enums/node-transformers/NodeTransformer';
import { NodeTransformationStage } from './enums/node-transformers/NodeTransformationStage';

import { ecmaVersion } from './constants/EcmaVersion';

import { ASTParserFacade } from './ASTParserFacade';
import { NodeGuards } from './node/NodeGuards';
import { Utils } from './utils/Utils';

@injectable()
export class JavaScriptObfuscator implements IJavaScriptObfuscator {
    /**
     * @type {Options}
     */
    private static readonly parseOptions: acorn.Options = {
        ecmaVersion,
        allowHashBang: true,
        allowImportExportEverywhere: true,
        allowReturnOutsideFunction: true,
        locations: true,
        ranges: true
    };

    /**
     * @type {GenerateOptions}
     */
    private static readonly escodegenParams: escodegen.GenerateOptions = {
        comment: true,
        verbatim: 'x-verbatim-property',
        sourceMapWithCode: true
    };

    /**
     * @type {CodeTransformer[]}
     */
    private static readonly codeTransformersList: CodeTransformer[] = [
        CodeTransformer.HashbangOperatorTransformer
    ];

    /**
     * @type {NodeTransformer[]}
     */
    private static readonly nodeTransformersList: NodeTransformer[] = [
        NodeTransformer.BlockStatementControlFlowTransformer,
        NodeTransformer.CommentsTransformer,
        NodeTransformer.CustomCodeHelpersTransformer,
        NodeTransformer.DeadCodeInjectionTransformer,
        NodeTransformer.EvalCallExpressionTransformer,
        NodeTransformer.FunctionControlFlowTransformer,
        NodeTransformer.LabeledStatementTransformer,
        NodeTransformer.LiteralTransformer,
        NodeTransformer.MemberExpressionTransformer,
        NodeTransformer.MetadataTransformer,
        NodeTransformer.MethodDefinitionTransformer,
        NodeTransformer.ObfuscatingGuardsTransformer,
        NodeTransformer.ObjectExpressionKeysTransformer,
        NodeTransformer.ObjectExpressionTransformer,
        NodeTransformer.ParentificationTransformer,
        NodeTransformer.ScopeIdentifiersTransformer,
        NodeTransformer.SplitStringTransformer,
        NodeTransformer.TemplateLiteralTransformer,
        NodeTransformer.VariablePreserveTransformer
    ];

    /**
     * @type {ICodeTransformersRunner}
     */
    private readonly codeTransformersRunner: ICodeTransformersRunner;

    /**
     * @type {ILogger}
     */
    private readonly logger: ILogger;

    /**
     * @type {TObfuscatedCodeFactory}
     */
    private readonly obfuscatedCodeFactory: TObfuscatedCodeFactory;

    /**
     * @type {IOptions}
     */
    private readonly options: IOptions;

    /**
     * @type {IRandomGenerator}
     */
    private readonly randomGenerator: IRandomGenerator;

    /**
     * @type {INodeTransformersRunner}
     */
    private readonly nodeTransformersRunner: INodeTransformersRunner;

    /**
     * @param {ICodeTransformersRunner} codeTransformersRunner
     * @param {INodeTransformersRunner} nodeTransformersRunner
     * @param {IRandomGenerator} randomGenerator
     * @param {TObfuscatedCodeFactory} obfuscatedCodeFactory
     * @param {ILogger} logger
     * @param {IOptions} options
     */
    public constructor (
        @inject(ServiceIdentifiers.ICodeTransformersRunner) codeTransformersRunner: ICodeTransformersRunner,
        @inject(ServiceIdentifiers.INodeTransformersRunner) nodeTransformersRunner: INodeTransformersRunner,
        @inject(ServiceIdentifiers.IRandomGenerator) randomGenerator: IRandomGenerator,
        @inject(ServiceIdentifiers.Factory__IObfuscatedCode) obfuscatedCodeFactory: TObfuscatedCodeFactory,
        @inject(ServiceIdentifiers.ILogger) logger: ILogger,
        @inject(ServiceIdentifiers.IOptions) options: IOptions
    ) {
        this.codeTransformersRunner = codeTransformersRunner;
        this.nodeTransformersRunner = nodeTransformersRunner;
        this.randomGenerator = randomGenerator;
        this.obfuscatedCodeFactory = obfuscatedCodeFactory;
        this.logger = logger;
        this.options = options;
    }

    /**
     * @param {string} sourceCode
     * @returns {IObfuscatedCode}
     */
    public obfuscate (sourceCode: string): IObfuscatedCode {
        const timeStart: number = Date.now();
        this.logger.info(LoggingMessage.Version, Utils.buildVersionMessage(process.env.VERSION, process.env.BUILD_TIMESTAMP));
        this.logger.info(LoggingMessage.ObfuscationStarted);
        this.logger.info(LoggingMessage.RandomGeneratorSeed, this.randomGenerator.getInputSeed());

        // preparing code transformations
        sourceCode = this.runCodeTransformationStage(sourceCode, CodeTransformationStage.PreparingTransformers);

        // parse AST tree
        const astTree: ESTree.Program = this.parseCode(sourceCode);

        // obfuscate AST tree
        const obfuscatedAstTree: ESTree.Program = this.transformAstTree(astTree);

        // generate code
        const generatorOutput: IGeneratorOutput = this.generateCode(sourceCode, obfuscatedAstTree);

        // finalizing code transformations
        generatorOutput.code = this.runCodeTransformationStage(generatorOutput.code, CodeTransformationStage.FinalizingTransformers);

        const obfuscationTime: number = (Date.now() - timeStart) / 1000;
        this.logger.success(LoggingMessage.ObfuscationCompleted, obfuscationTime);

        return this.getObfuscatedCode(generatorOutput);
    }

    /**
     * @param {string} sourceCode
     * @returns {Program}
     */
    private parseCode (sourceCode: string): ESTree.Program {
        return ASTParserFacade.parse(sourceCode, JavaScriptObfuscator.parseOptions);
    }

    /**
     * @param {Program} astTree
     * @returns {Program}
     */
    private transformAstTree (astTree: ESTree.Program): ESTree.Program {
        astTree = this.runNodeTransformationStage(astTree, NodeTransformationStage.Initializing);

        const isEmptyAstTree: boolean = NodeGuards.isProgramNode(astTree)
            && !astTree.body.length
            && !astTree.leadingComments
            && !astTree.trailingComments;

        if (isEmptyAstTree) {
            this.logger.warn(LoggingMessage.EmptySourceCode);

            return astTree;
        }

        astTree = this.runNodeTransformationStage(astTree, NodeTransformationStage.Preparing);

        if (this.options.deadCodeInjection) {
            astTree = this.runNodeTransformationStage(astTree, NodeTransformationStage.DeadCodeInjection);
        }

        if (this.options.controlFlowFlattening) {
            astTree = this.runNodeTransformationStage(astTree, NodeTransformationStage.ControlFlowFlattening);
        }

        astTree = this.runNodeTransformationStage(astTree, NodeTransformationStage.Converting);
        astTree = this.runNodeTransformationStage(astTree, NodeTransformationStage.Obfuscating);
        astTree = this.runNodeTransformationStage(astTree, NodeTransformationStage.Finalizing);

        return astTree;
    }

    /**
     * @param {string} sourceCode
     * @param {Program} astTree
     * @returns {IGeneratorOutput}
     */
    private generateCode (sourceCode: string, astTree: ESTree.Program): IGeneratorOutput {
        const escodegenParams: escodegen.GenerateOptions = {
            ...JavaScriptObfuscator.escodegenParams
        };

        if (this.options.sourceMap) {
            escodegenParams.sourceMap = this.options.inputFileName || 'sourceMap';
            escodegenParams.sourceContent = sourceCode;
        }

        const generatorOutput: IGeneratorOutput = escodegen.generate(astTree, {
            ...escodegenParams,
            format: {
                compact: this.options.compact
            }
        });

        generatorOutput.map = generatorOutput.map ? generatorOutput.map.toString() : '';

        return generatorOutput;
    }

    /**
     * @param {IGeneratorOutput} generatorOutput
     * @returns {IObfuscatedCode}
     */
    private getObfuscatedCode (generatorOutput: IGeneratorOutput): IObfuscatedCode {
        return this.obfuscatedCodeFactory(generatorOutput.code, generatorOutput.map);
    }

    /**
     * @param {string} code
     * @param {CodeTransformationStage} codeTransformationStage
     * @returns {string}
     */
    private runCodeTransformationStage (code: string, codeTransformationStage: CodeTransformationStage): string {
        this.logger.info(LoggingMessage.CodeTransformationStage, codeTransformationStage);

        return this.codeTransformersRunner.transform(
            code,
            JavaScriptObfuscator.codeTransformersList,
            codeTransformationStage
        );
    }

    /**
     * @param {Program} astTree
     * @param {NodeTransformationStage} nodeTransformationStage
     * @returns {Program}
     */
    private runNodeTransformationStage (astTree: ESTree.Program, nodeTransformationStage: NodeTransformationStage): ESTree.Program {
        this.logger.info(LoggingMessage.NodeTransformationStage, nodeTransformationStage);

        return this.nodeTransformersRunner.transform(
            astTree,
            JavaScriptObfuscator.nodeTransformersList,
            nodeTransformationStage
        );
    }
}
