/* @internal */
namespace ts {
    function getModuleTransformer(moduleKind: ModuleKind): TransformerFactory<SourceFile | Bundle> {
        switch (moduleKind) {
            case ModuleKind.ESNext:
            case ModuleKind.ES2020:
            case ModuleKind.ES2015:
                return transformECMAScriptModule;
            case ModuleKind.System:
                return transformSystemModule;
            default:
                return transformModule;
        }
    }

    const enum TransformationState {
        Uninitialized,
        Initialized,
        Completed,
        Disposed
    }

    const enum SyntaxKindFeatureFlags {
        Substitution = 1 << 0,
        EmitNotifications = 1 << 1,
    }

    export const noTransformers: EmitTransformers = { scriptTransformers: emptyArray, declarationTransformers: emptyArray };

    export function getTransformers(compilerOptions: CompilerOptions, customTransformers?: CustomTransformers, emitOnlyDtsFiles?: boolean): EmitTransformers {
        return {
            scriptTransformers: getScriptTransformers(compilerOptions, customTransformers, emitOnlyDtsFiles),
            declarationTransformers: getDeclarationTransformers(customTransformers),
        };
    }

    function getScriptTransformers(compilerOptions: CompilerOptions, customTransformers?: CustomTransformers, emitOnlyDtsFiles?: boolean) {
        if (emitOnlyDtsFiles) return emptyArray;

        const languageVersion = getEmitScriptTarget(compilerOptions);
        const moduleKind = getEmitModuleKind(compilerOptions);
        const transformers: TransformerFactory<SourceFile | Bundle>[] = [];

        addRange(transformers, customTransformers && map(customTransformers.before, wrapScriptTransformerFactory));

        transformers.push(transformTypeScript);
        transformers.push(transformClassFields);

        if (getJSXTransformEnabled(compilerOptions)) {
            transformers.push(transformJsx);
        }

        if (languageVersion < ScriptTarget.ESNext) {
            transformers.push(transformESNext);
        }

        if (languageVersion < ScriptTarget.ES2020) {
            transformers.push(transformES2020);
        }

        if (languageVersion < ScriptTarget.ES2019) {
            transformers.push(transformES2019);
        }

        if (languageVersion < ScriptTarget.ES2018) {
            transformers.push(transformES2018);
        }

        if (languageVersion < ScriptTarget.ES2017) {
            transformers.push(transformES2017);
        }

        if (languageVersion < ScriptTarget.ES2016) {
            transformers.push(transformES2016);
        }

        if (languageVersion < ScriptTarget.ES2015) {
            transformers.push(transformES2015);
            transformers.push(transformGenerators);
        }

        transformers.push(getModuleTransformer(moduleKind));

        // The ES5 transformer is last so that it can substitute expressions like `exports.default`
        // for ES3.
        if (languageVersion < ScriptTarget.ES5) {
            transformers.push(transformES5);
        }

        addRange(transformers, customTransformers && map(customTransformers.after, wrapScriptTransformerFactory));
        return transformers;
    }

    function getDeclarationTransformers(customTransformers?: CustomTransformers) {
        const transformers: TransformerFactory<SourceFile | Bundle>[] = [];
        transformers.push(transformDeclarations);
        addRange(transformers, customTransformers && map(customTransformers.afterDeclarations, wrapDeclarationTransformerFactory));
        return transformers;
    }

    /**
     * Wrap a custom script or declaration transformer object in a `Transformer` callback with fallback support for transforming bundles.
     */
    function wrapCustomTransformer(transformer: CustomTransformer): Transformer<Bundle | SourceFile> {
        return node => isBundle(node) ? transformer.transformBundle(node) : transformer.transformSourceFile(node);
    }

    /**
     * Wrap a transformer factory that may return a custom script or declaration transformer object.
     */
    function wrapCustomTransformerFactory<T extends SourceFile | Bundle>(transformer: TransformerFactory<T> | CustomTransformerFactory, handleDefault: (context: TransformationContext, tx: Transformer<T>) => Transformer<Bundle | SourceFile>): TransformerFactory<Bundle | SourceFile> {
        return context => {
            const customTransformer = transformer(context);
            return typeof customTransformer === "function"
                ? handleDefault(context, customTransformer)
                : wrapCustomTransformer(customTransformer);
        };
    }

    function wrapScriptTransformerFactory(transformer: TransformerFactory<SourceFile> | CustomTransformerFactory): TransformerFactory<Bundle | SourceFile> {
        return wrapCustomTransformerFactory(transformer, chainBundle);
    }

    function wrapDeclarationTransformerFactory(transformer: TransformerFactory<Bundle | SourceFile> | CustomTransformerFactory): TransformerFactory<Bundle | SourceFile> {
        return wrapCustomTransformerFactory(transformer, (_, node) => node);
    }

    export function noEmitSubstitution(_hint: EmitHint, node: Node) {
        return node;
    }

    export function noEmitNotification(hint: EmitHint, node: Node, callback: (hint: EmitHint, node: Node) => void) {
        callback(hint, node);
    }

    /**
     * Transforms an array of SourceFiles by passing them through each transformer.
     *
     * @param resolver The emit resolver provided by the checker.
     * @param host The emit host object used to interact with the file system.
     * @param options Compiler options to surface in the `TransformationContext`.
     * @param nodes An array of nodes to transform.
     * @param transforms An array of `TransformerFactory` callbacks.
     * @param allowDtsFiles A value indicating whether to allow the transformation of .d.ts files.
     */
    export function transformNodes<T extends Node>(resolver: EmitResolver | undefined, host: EmitHost | undefined, factory: NodeFactory, options: CompilerOptions, nodes: readonly T[], transformers: readonly TransformerFactory<T>[], allowDtsFiles: boolean): TransformationResult<T> {
        const enabledSyntaxKindFeatures = new Array<SyntaxKindFeatureFlags>(SyntaxKind.Count);
        let lexicalEnvironmentVariableDeclarations: VariableDeclaration[];
        let lexicalEnvironmentFunctionDeclarations: FunctionDeclaration[];
        let lexicalEnvironmentStatements: Statement[];
        let lexicalEnvironmentFlags = LexicalEnvironmentFlags.None;
        let lexicalEnvironmentVariableDeclarationsStack: VariableDeclaration[][] = [];
        let lexicalEnvironmentFunctionDeclarationsStack: FunctionDeclaration[][] = [];
        let lexicalEnvironmentStatementsStack: Statement[][] = [];
        let lexicalEnvironmentFlagsStack: LexicalEnvironmentFlags[] = [];
        let lexicalEnvironmentStackOffset = 0;
        let lexicalEnvironmentSuspended = false;
        let emitHelpers: EmitHelper[] | undefined;
        let onSubstituteNode: TransformationContext["onSubstituteNode"] = noEmitSubstitution;
        let onEmitNode: TransformationContext["onEmitNode"] = noEmitNotification;
        let state = TransformationState.Uninitialized;
        const diagnostics: DiagnosticWithLocation[] = [];

        // The transformation context is provided to each transformer as part of transformer
        // initialization.
        const context: TransformationContext = {
            factory,
            getCompilerOptions: () => options,
            getEmitResolver: () => resolver!, // TODO: GH#18217
            getEmitHost: () => host!, // TODO: GH#18217
            getEmitHelperFactory: memoize(() => createEmitHelperFactory(context)),
            startLexicalEnvironment,
            suspendLexicalEnvironment,
            resumeLexicalEnvironment,
            endLexicalEnvironment,
            setLexicalEnvironmentFlags,
            getLexicalEnvironmentFlags,
            hoistVariableDeclaration,
            hoistFunctionDeclaration,
            addInitializationStatement,
            requestEmitHelper,
            readEmitHelpers,
            enableSubstitution,
            enableEmitNotification,
            isSubstitutionEnabled,
            isEmitNotificationEnabled,
            get onSubstituteNode() { return onSubstituteNode; },
            set onSubstituteNode(value) {
                Debug.assert(state < TransformationState.Initialized, "Cannot modify transformation hooks after initialization has completed.");
                Debug.assert(value !== undefined, "Value must not be 'undefined'");
                onSubstituteNode = value;
            },
            get onEmitNode() { return onEmitNode; },
            set onEmitNode(value) {
                Debug.assert(state < TransformationState.Initialized, "Cannot modify transformation hooks after initialization has completed.");
                Debug.assert(value !== undefined, "Value must not be 'undefined'");
                onEmitNode = value;
            },
            addDiagnostic(diag) {
                diagnostics.push(diag);
            }
        };

        // Ensure the parse tree is clean before applying transformations
        for (const node of nodes) {
            disposeEmitNodes(getSourceFileOfNode(getParseTreeNode(node)));
        }

        performance.mark("beforeTransform");

        // Chain together and initialize each transformer.
        const transformersWithContext = transformers.map(t => t(context));
        const transformation = (node: T): T => {
            for (const transform of transformersWithContext) {
                node = transform(node);
            }
            return node;
        };

        // prevent modification of transformation hooks.
        state = TransformationState.Initialized;

        // Transform each node.
        const transformed: T[] = [];
        for (const node of nodes) {
            tracing?.push(tracing.Phase.Emit, "transformNodes", node.kind === SyntaxKind.SourceFile ? { path: (node as any as SourceFile).path } : { kind: node.kind, pos: node.pos, end: node.end });
            transformed.push((allowDtsFiles ? transformation : transformRoot)(node));
            tracing?.pop();
        }

        // prevent modification of the lexical environment.
        state = TransformationState.Completed;

        performance.mark("afterTransform");
        performance.measure("transformTime", "beforeTransform", "afterTransform");

        return {
            transformed,
            substituteNode,
            emitNodeWithNotification,
            isEmitNotificationEnabled,
            dispose,
            diagnostics
        };

        function transformRoot(node: T) {
            return node && (!isSourceFile(node) || !node.isDeclarationFile) ? transformation(node) : node;
        }

        /**
         * Enables expression substitutions in the pretty printer for the provided SyntaxKind.
         */
        function enableSubstitution(kind: SyntaxKind) {
            Debug.assert(state < TransformationState.Completed, "Cannot modify the transformation context after transformation has completed.");
            enabledSyntaxKindFeatures[kind] |= SyntaxKindFeatureFlags.Substitution;
        }

        /**
         * Determines whether expression substitutions are enabled for the provided node.
         */
        function isSubstitutionEnabled(node: Node) {
            return (enabledSyntaxKindFeatures[node.kind] & SyntaxKindFeatureFlags.Substitution) !== 0
                && (getEmitFlags(node) & EmitFlags.NoSubstitution) === 0;
        }

        /**
         * Emits a node with possible substitution.
         *
         * @param hint A hint as to the intended usage of the node.
         * @param node The node to emit.
         * @param emitCallback The callback used to emit the node or its substitute.
         */
        function substituteNode(hint: EmitHint, node: Node) {
            Debug.assert(state < TransformationState.Disposed, "Cannot substitute a node after the result is disposed.");
            return node && isSubstitutionEnabled(node) && onSubstituteNode(hint, node) || node;
        }

        /**
         * Enables before/after emit notifications in the pretty printer for the provided SyntaxKind.
         */
        function enableEmitNotification(kind: SyntaxKind) {
            Debug.assert(state < TransformationState.Completed, "Cannot modify the transformation context after transformation has completed.");
            enabledSyntaxKindFeatures[kind] |= SyntaxKindFeatureFlags.EmitNotifications;
        }

        /**
         * Determines whether before/after emit notifications should be raised in the pretty
         * printer when it emits a node.
         */
        function isEmitNotificationEnabled(node: Node) {
            return (enabledSyntaxKindFeatures[node.kind] & SyntaxKindFeatureFlags.EmitNotifications) !== 0
                || (getEmitFlags(node) & EmitFlags.AdviseOnEmitNode) !== 0;
        }

        /**
         * Emits a node with possible emit notification.
         *
         * @param hint A hint as to the intended usage of the node.
         * @param node The node to emit.
         * @param emitCallback The callback used to emit the node.
         */
        function emitNodeWithNotification(hint: EmitHint, node: Node, emitCallback: (hint: EmitHint, node: Node) => void) {
            Debug.assert(state < TransformationState.Disposed, "Cannot invoke TransformationResult callbacks after the result is disposed.");
            if (node) {
                // TODO: Remove check and unconditionally use onEmitNode when API is breakingly changed
                // (see https://github.com/microsoft/TypeScript/pull/36248/files/5062623f39120171b98870c71344b3242eb03d23#r369766739)
                if (isEmitNotificationEnabled(node)) {
                    onEmitNode(hint, node, emitCallback);
                }
                else {
                    emitCallback(hint, node);
                }
            }
        }

        /**
         * Records a hoisted variable declaration for the provided name within a lexical environment.
         */
        function hoistVariableDeclaration(name: Identifier): void {
            Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization.");
            Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed.");
            const decl = setEmitFlags(factory.createVariableDeclaration(name), EmitFlags.NoNestedSourceMaps);
            if (!lexicalEnvironmentVariableDeclarations) {
                lexicalEnvironmentVariableDeclarations = [decl];
            }
            else {
                lexicalEnvironmentVariableDeclarations.push(decl);
            }
            if (lexicalEnvironmentFlags & LexicalEnvironmentFlags.InParameters) {
                lexicalEnvironmentFlags |= LexicalEnvironmentFlags.VariablesHoistedInParameters;
            }
        }

        /**
         * Records a hoisted function declaration within a lexical environment.
         */
        function hoistFunctionDeclaration(func: FunctionDeclaration): void {
            Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization.");
            Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed.");
            setEmitFlags(func, EmitFlags.CustomPrologue);
            if (!lexicalEnvironmentFunctionDeclarations) {
                lexicalEnvironmentFunctionDeclarations = [func];
            }
            else {
                lexicalEnvironmentFunctionDeclarations.push(func);
            }
        }

        /**
         * Adds an initialization statement to the top of the lexical environment.
         */
        function addInitializationStatement(node: Statement): void {
            Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization.");
            Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed.");
            setEmitFlags(node, EmitFlags.CustomPrologue);
            if (!lexicalEnvironmentStatements) {
                lexicalEnvironmentStatements = [node];
            }
            else {
                lexicalEnvironmentStatements.push(node);
            }
        }

        /**
         * Starts a new lexical environment. Any existing hoisted variable or function declarations
         * are pushed onto a stack, and the related storage variables are reset.
         */
        function startLexicalEnvironment(): void {
            Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization.");
            Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed.");
            Debug.assert(!lexicalEnvironmentSuspended, "Lexical environment is suspended.");

            // Save the current lexical environment. Rather than resizing the array we adjust the
            // stack size variable. This allows us to reuse existing array slots we've
            // already allocated between transformations to avoid allocation and GC overhead during
            // transformation.
            lexicalEnvironmentVariableDeclarationsStack[lexicalEnvironmentStackOffset] = lexicalEnvironmentVariableDeclarations;
            lexicalEnvironmentFunctionDeclarationsStack[lexicalEnvironmentStackOffset] = lexicalEnvironmentFunctionDeclarations;
            lexicalEnvironmentStatementsStack[lexicalEnvironmentStackOffset] = lexicalEnvironmentStatements;
            lexicalEnvironmentFlagsStack[lexicalEnvironmentStackOffset] = lexicalEnvironmentFlags;
            lexicalEnvironmentStackOffset++;
            lexicalEnvironmentVariableDeclarations = undefined!;
            lexicalEnvironmentFunctionDeclarations = undefined!;
            lexicalEnvironmentStatements = undefined!;
            lexicalEnvironmentFlags = LexicalEnvironmentFlags.None;
        }

        /** Suspends the current lexical environment, usually after visiting a parameter list. */
        function suspendLexicalEnvironment(): void {
            Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization.");
            Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed.");
            Debug.assert(!lexicalEnvironmentSuspended, "Lexical environment is already suspended.");
            lexicalEnvironmentSuspended = true;
        }

        /** Resumes a suspended lexical environment, usually before visiting a function body. */
        function resumeLexicalEnvironment(): void {
            Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization.");
            Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed.");
            Debug.assert(lexicalEnvironmentSuspended, "Lexical environment is not suspended.");
            lexicalEnvironmentSuspended = false;
        }

        /**
         * Ends a lexical environment. The previous set of hoisted declarations are restored and
         * any hoisted declarations added in this environment are returned.
         */
        function endLexicalEnvironment(): Statement[] | undefined {
            Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the lexical environment during initialization.");
            Debug.assert(state < TransformationState.Completed, "Cannot modify the lexical environment after transformation has completed.");
            Debug.assert(!lexicalEnvironmentSuspended, "Lexical environment is suspended.");

            let statements: Statement[] | undefined;
            if (lexicalEnvironmentVariableDeclarations ||
                lexicalEnvironmentFunctionDeclarations ||
                lexicalEnvironmentStatements) {
                if (lexicalEnvironmentFunctionDeclarations) {
                    statements = [...lexicalEnvironmentFunctionDeclarations];
                }

                if (lexicalEnvironmentVariableDeclarations) {
                    const statement = factory.createVariableStatement(
                        /*modifiers*/ undefined,
                        factory.createVariableDeclarationList(lexicalEnvironmentVariableDeclarations)
                    );

                    setEmitFlags(statement, EmitFlags.CustomPrologue);

                    if (!statements) {
                        statements = [statement];
                    }
                    else {
                        statements.push(statement);
                    }
                }

                if (lexicalEnvironmentStatements) {
                    if (!statements) {
                        statements = [...lexicalEnvironmentStatements];
                    }
                    else {
                        statements = [...statements, ...lexicalEnvironmentStatements];
                    }
                }
            }

            // Restore the previous lexical environment.
            lexicalEnvironmentStackOffset--;
            lexicalEnvironmentVariableDeclarations = lexicalEnvironmentVariableDeclarationsStack[lexicalEnvironmentStackOffset];
            lexicalEnvironmentFunctionDeclarations = lexicalEnvironmentFunctionDeclarationsStack[lexicalEnvironmentStackOffset];
            lexicalEnvironmentStatements = lexicalEnvironmentStatementsStack[lexicalEnvironmentStackOffset];
            lexicalEnvironmentFlags = lexicalEnvironmentFlagsStack[lexicalEnvironmentStackOffset];
            if (lexicalEnvironmentStackOffset === 0) {
                lexicalEnvironmentVariableDeclarationsStack = [];
                lexicalEnvironmentFunctionDeclarationsStack = [];
                lexicalEnvironmentStatementsStack = [];
                lexicalEnvironmentFlagsStack = [];
            }
            return statements;
        }

        function setLexicalEnvironmentFlags(flags: LexicalEnvironmentFlags, value: boolean): void {
            lexicalEnvironmentFlags = value ?
                lexicalEnvironmentFlags | flags :
                lexicalEnvironmentFlags & ~flags;
        }

        function getLexicalEnvironmentFlags(): LexicalEnvironmentFlags {
            return lexicalEnvironmentFlags;
        }

        function requestEmitHelper(helper: EmitHelper): void {
            Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the transformation context during initialization.");
            Debug.assert(state < TransformationState.Completed, "Cannot modify the transformation context after transformation has completed.");
            Debug.assert(!helper.scoped, "Cannot request a scoped emit helper.");
            if (helper.dependencies) {
                for (const h of helper.dependencies) {
                    requestEmitHelper(h);
                }
            }
            emitHelpers = append(emitHelpers, helper);
        }

        function readEmitHelpers(): EmitHelper[] | undefined {
            Debug.assert(state > TransformationState.Uninitialized, "Cannot modify the transformation context during initialization.");
            Debug.assert(state < TransformationState.Completed, "Cannot modify the transformation context after transformation has completed.");
            const helpers = emitHelpers;
            emitHelpers = undefined;
            return helpers;
        }

        function dispose() {
            if (state < TransformationState.Disposed) {
                // Clean up emit nodes on parse tree
                for (const node of nodes) {
                    disposeEmitNodes(getSourceFileOfNode(getParseTreeNode(node)));
                }

                // Release references to external entries for GC purposes.
                lexicalEnvironmentVariableDeclarations = undefined!;
                lexicalEnvironmentVariableDeclarationsStack = undefined!;
                lexicalEnvironmentFunctionDeclarations = undefined!;
                lexicalEnvironmentFunctionDeclarationsStack = undefined!;
                onSubstituteNode = undefined!;
                onEmitNode = undefined!;
                emitHelpers = undefined;

                // Prevent further use of the transformation result.
                state = TransformationState.Disposed;
            }
        }
    }

    export const nullTransformationContext: TransformationContext = {
        get factory() { return factory; },
        enableEmitNotification: noop,
        enableSubstitution: noop,
        endLexicalEnvironment: returnUndefined,
        getCompilerOptions: () => ({}),
        getEmitHost: notImplemented,
        getEmitResolver: notImplemented,
        getEmitHelperFactory: notImplemented,
        setLexicalEnvironmentFlags: noop,
        getLexicalEnvironmentFlags: () => 0,
        hoistFunctionDeclaration: noop,
        hoistVariableDeclaration: noop,
        addInitializationStatement: noop,
        isEmitNotificationEnabled: notImplemented,
        isSubstitutionEnabled: notImplemented,
        onEmitNode: noop,
        onSubstituteNode: notImplemented,
        readEmitHelpers: notImplemented,
        requestEmitHelper: noop,
        resumeLexicalEnvironment: noop,
        startLexicalEnvironment: noop,
        suspendLexicalEnvironment: noop,
        addDiagnostic: noop,
    };
}
