import generate from '@babel/generator';
import * as t from '@babel/types';
import traverse from '@babel/traverse';
import { TransformationType } from './transformations/transformation';
import { Config, defaultConfig } from './transformations/config';
import { ObjectSimplifier } from './transformations/objects/objectSimplifier';
import { ProxyFunctionInliner } from './transformations/proxyFunctions/proxyFunctionInliner';
import { UnusedVariableRemover } from './transformations/variables/unusedVariableRemover';
import { ConstantPropgator } from './transformations/variables/constantPropagator';
import { ReassignmentRemover } from './transformations/variables/reassignmentRemover';
import { StringRevealer } from './transformations/strings/stringRevealer';
import { DeadBranchRemover } from './transformations/controlFlow/deadBranchRemover';
import { SequenceSplitter } from './transformations/controlFlow/sequenceSplitter';
import { PropertySimplifier } from './transformations/properties/propertySimplifier';
import { ExpressionSimplifier } from './transformations/expressions/expressionSimplifier';
import { ControlFlowRecoverer } from './transformations/controlFlow/controlFlowRecoverer';
import { ObjectPacker } from './transformations/objects/objectPacker';
import { AntiTamperRemover } from './transformations/antiTamper/antiTamperRemover';

export class Deobfuscator {
    private readonly ast: t.File;
    private readonly config: Config;
    private readonly transformationTypes: TransformationType[] = [
        UnusedVariableRemover,
        ConstantPropgator,
        ReassignmentRemover,
        DeadBranchRemover,
        ObjectPacker,
        ProxyFunctionInliner,
        ExpressionSimplifier,
        SequenceSplitter,
        ControlFlowRecoverer,
        PropertySimplifier,
        AntiTamperRemover,
        ObjectSimplifier,
        StringRevealer
    ];
    private static readonly MAX_ITERATIONS = 50;

    /**
     * Creates a new deobfuscator.
     * @param ast The AST.
     * @param config The config (optional).
     */
    constructor(ast: t.File, config: Config = defaultConfig) {
        this.ast = ast;
        this.config = config;
    }

    /**
     * Executes the deobfuscator.
     * @returns The simplified code.
     */
    public execute(): string {
        let types = this.transformationTypes.filter(t => this.config[t.properties.key].isEnabled);
        let i = 0;

        while (i < Deobfuscator.MAX_ITERATIONS) {
            let isModified = false;

            if (!this.config.silent) {
                console.log(`\n[${new Date().toISOString()}]: Starting pass ${i + 1}`);
            }
            for (const type of types) {
                const transformationConfig = this.config[type.properties.key];
                const transformation = new type(this.ast, transformationConfig);

                if (!this.config.silent) {
                    console.log(
                        `[${new Date().toISOString()}]: Executing ${
                            transformation.constructor.name
                        }`
                    );
                }

                let modified = false;
                try {
                    modified = transformation.execute(
                        console.log.bind(console, `[${transformation.constructor.name}]:`)
                    );
                } catch (err) {
                    console.error(err);
                }

                if (modified) {
                    isModified = true;
                }

                if (!this.config.silent) {
                    console.log(
                        `[${new Date().toISOString()}]: Executed ${
                            transformation.constructor.name
                        }, modified ${modified}`
                    );
                }

                if (type.properties.rebuildScopeTree) {
                    this.clearCache();
                }
            }

            i++;
            if (!isModified) {
                break;
            }
        }

        return generate(this.ast, { jsescOption: { minimal: true } }).code;
    }

    /**
     * Clears the traversal cache to force the scoping to be handled
     * again on the next traverse.
     */
    private clearCache(): void {
        (traverse as any).cache.clear();
    }
}
