"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/**
 * @license
 * Copyright Google Inc. All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */
const ts = require("typescript");
const ast_utils_1 = require("../helpers/ast-utils");
function isBlockLike(node) {
    return node.kind === ts.SyntaxKind.Block
        || node.kind === ts.SyntaxKind.ModuleBlock
        || node.kind === ts.SyntaxKind.CaseClause
        || node.kind === ts.SyntaxKind.DefaultClause
        || node.kind === ts.SyntaxKind.SourceFile;
}
function getWrapEnumsTransformer() {
    return (context) => {
        const transformer = sf => {
            const result = visitBlockStatements(sf.statements, context);
            return ts.updateSourceFileNode(sf, ts.setTextRange(result, sf.statements));
        };
        return transformer;
    };
}
exports.getWrapEnumsTransformer = getWrapEnumsTransformer;
function visitBlockStatements(statements, context) {
    // copy of statements to modify; lazy initialized
    let updatedStatements;
    const visitor = (node) => {
        if (isBlockLike(node)) {
            let result = visitBlockStatements(node.statements, context);
            if (result === node.statements) {
                return node;
            }
            result = ts.setTextRange(result, node.statements);
            switch (node.kind) {
                case ts.SyntaxKind.Block:
                    return ts.updateBlock(node, result);
                case ts.SyntaxKind.ModuleBlock:
                    return ts.updateModuleBlock(node, result);
                case ts.SyntaxKind.CaseClause:
                    return ts.updateCaseClause(node, node.expression, result);
                case ts.SyntaxKind.DefaultClause:
                    return ts.updateDefaultClause(node, result);
                default:
                    return node;
            }
        }
        else {
            return node;
        }
    };
    // 'oIndex' is the original statement index; 'uIndex' is the updated statement index
    for (let oIndex = 0, uIndex = 0; oIndex < statements.length - 1; oIndex++, uIndex++) {
        const currentStatement = statements[oIndex];
        let newStatement;
        let oldStatementsLength = 0;
        // these can't contain an enum declaration
        if (currentStatement.kind === ts.SyntaxKind.ImportDeclaration) {
            continue;
        }
        // enum declarations must:
        //   * not be last statement
        //   * be a variable statement
        //   * have only one declaration
        //   * have an identifer as a declaration name
        // ClassExpression declarations must:
        //   * not be last statement
        //   * be a variable statement
        //   * have only one declaration
        //   * have an ClassExpression or BinaryExpression and a right
        //     of kind ClassExpression as a initializer
        if (ts.isVariableStatement(currentStatement)
            && currentStatement.declarationList.declarations.length === 1) {
            const variableDeclaration = currentStatement.declarationList.declarations[0];
            const initializer = variableDeclaration.initializer;
            if (ts.isIdentifier(variableDeclaration.name)) {
                const name = variableDeclaration.name.text;
                if (!initializer) {
                    const iife = findTs2_3EnumIife(name, statements[oIndex + 1]);
                    if (iife) {
                        // update IIFE and replace variable statement and old IIFE
                        oldStatementsLength = 2;
                        newStatement = updateEnumIife(currentStatement, iife[0], iife[1]);
                        // skip IIFE statement
                        oIndex++;
                    }
                }
                else if (ts.isObjectLiteralExpression(initializer)) {
                    // tsickle es2015 enums first statement is an export declaration
                    const isPotentialEnumExport = ts.isExportDeclaration(statements[oIndex + 1]);
                    if (isPotentialEnumExport) {
                        // skip the export
                        oIndex++;
                    }
                    const enumStatements = findStatements(name, statements, oIndex, 1);
                    if (!enumStatements) {
                        continue;
                    }
                    // create wrapper and replace variable statement and enum member statements
                    oldStatementsLength = enumStatements.length + (isPotentialEnumExport ? 2 : 1);
                    newStatement = createWrappedEnum(name, currentStatement, enumStatements, initializer, isPotentialEnumExport);
                    // skip enum member declarations
                    oIndex += enumStatements.length;
                }
                else if (ts.isClassExpression(initializer)
                    || (ts.isBinaryExpression(initializer)
                        && ts.isClassExpression(initializer.right))) {
                    const classStatements = findStatements(name, statements, oIndex);
                    if (!classStatements) {
                        continue;
                    }
                    oldStatementsLength = classStatements.length;
                    newStatement = createWrappedClass(variableDeclaration, classStatements);
                    oIndex += classStatements.length - 1;
                }
            }
        }
        else if (ts.isClassDeclaration(currentStatement)) {
            const name = currentStatement.name.text;
            const classStatements = findStatements(name, statements, oIndex);
            if (!classStatements) {
                continue;
            }
            oldStatementsLength = classStatements.length;
            newStatement = createWrappedClass(currentStatement, classStatements);
            oIndex += classStatements.length - 1;
        }
        if (newStatement && newStatement.length > 0) {
            if (!updatedStatements) {
                updatedStatements = [...statements];
            }
            updatedStatements.splice(uIndex, oldStatementsLength, ...newStatement);
            // When having more than a single new statement
            // we need to update the update Index
            uIndex += (newStatement ? newStatement.length - 1 : 0);
        }
        const result = ts.visitNode(currentStatement, visitor);
        if (result !== currentStatement) {
            if (!updatedStatements) {
                updatedStatements = statements.slice();
            }
            updatedStatements[uIndex] = result;
        }
    }
    // if changes, return updated statements
    // otherwise, return original array instance
    return updatedStatements ? ts.createNodeArray(updatedStatements) : statements;
}
// TS 2.3 enums have statements that are inside a IIFE.
function findTs2_3EnumIife(name, statement) {
    if (!ts.isExpressionStatement(statement)) {
        return null;
    }
    const expression = statement.expression;
    if (!expression || !ts.isCallExpression(expression) || expression.arguments.length !== 1) {
        return null;
    }
    const callExpression = expression;
    let exportExpression;
    if (!ts.isParenthesizedExpression(callExpression.expression)) {
        return null;
    }
    const functionExpression = callExpression.expression.expression;
    if (!ts.isFunctionExpression(functionExpression)) {
        return null;
    }
    // The name of the parameter can be different than the name of the enum if it was renamed
    // due to scope hoisting.
    const parameter = functionExpression.parameters[0];
    if (!ts.isIdentifier(parameter.name)) {
        return null;
    }
    const parameterName = parameter.name.text;
    let argument = callExpression.arguments[0];
    if (!ts.isBinaryExpression(argument)
        || !ts.isIdentifier(argument.left)
        || argument.left.text !== name) {
        return null;
    }
    let potentialExport = false;
    if (argument.operatorToken.kind === ts.SyntaxKind.FirstAssignment) {
        if (ts.isBinaryExpression(argument.right) && argument.right.operatorToken.kind !== ts.SyntaxKind.BarBarToken) {
            return null;
        }
        potentialExport = true;
        argument = argument.right;
    }
    if (!ts.isBinaryExpression(argument)) {
        return null;
    }
    if (argument.operatorToken.kind !== ts.SyntaxKind.BarBarToken) {
        return null;
    }
    if (potentialExport && !ts.isIdentifier(argument.left)) {
        exportExpression = argument.left;
    }
    // Go through all the statements and check that all match the name
    for (const statement of functionExpression.body.statements) {
        if (!ts.isExpressionStatement(statement)
            || !ts.isBinaryExpression(statement.expression)
            || !ts.isElementAccessExpression(statement.expression.left)) {
            return null;
        }
        const leftExpression = statement.expression.left.expression;
        if (!ts.isIdentifier(leftExpression) || leftExpression.text !== parameterName) {
            return null;
        }
    }
    return [callExpression, exportExpression];
}
function updateHostNode(hostNode, expression) {
    // Update existing host node with the pure comment before the variable declaration initializer.
    const variableDeclaration = hostNode.declarationList.declarations[0];
    const outerVarStmt = ts.updateVariableStatement(hostNode, hostNode.modifiers, ts.updateVariableDeclarationList(hostNode.declarationList, [
        ts.updateVariableDeclaration(variableDeclaration, variableDeclaration.name, variableDeclaration.type, expression),
    ]));
    return outerVarStmt;
}
/**
 * Find enums, class expression or declaration statements.
 *
 * The classExpressions block to wrap in an iife must
 * - end with an ExpressionStatement
 * - it's expression must be a BinaryExpression
 * - have the same name
 *
 * ```
 let Foo = class Foo {};
 Foo = __decorate([]);
 ```
 */
function findStatements(name, statements, statementIndex, offset = 0) {
    let count = 1;
    for (let index = statementIndex + 1; index < statements.length; ++index) {
        const statement = statements[index];
        if (!ts.isExpressionStatement(statement)) {
            break;
        }
        const expression = statement.expression;
        if (ts.isCallExpression(expression)) {
            // Ex:
            // setClassMetadata(FooClass, [{}], void 0);
            // __decorate([propDecorator()], FooClass.prototype, "propertyName", void 0);
            // __decorate([propDecorator()], FooClass, "propertyName", void 0);
            // __decorate$1([propDecorator()], FooClass, "propertyName", void 0);
            const args = expression.arguments;
            if (args.length > 2) {
                const isReferenced = args.some(arg => {
                    const potentialIdentifier = ts.isPropertyAccessExpression(arg) ? arg.expression : arg;
                    return ts.isIdentifier(potentialIdentifier) && potentialIdentifier.text === name;
                });
                if (isReferenced) {
                    count++;
                    continue;
                }
            }
        }
        else if (ts.isBinaryExpression(expression)) {
            const node = ts.isBinaryExpression(expression.left)
                ? expression.left.left
                : expression.left;
            const leftExpression = ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)
                // Static Properties // Ex: Foo.bar = 'value';
                // ENUM Property // Ex:  ChangeDetectionStrategy[ChangeDetectionStrategy.Default] = "Default";
                ? node.expression
                // Ex: FooClass = __decorate([Component()], FooClass);
                : node;
            if (ts.isIdentifier(leftExpression) && leftExpression.text === name) {
                count++;
                continue;
            }
        }
        break;
    }
    if (count > 1) {
        return statements.slice(statementIndex + offset, statementIndex + count);
    }
    return undefined;
}
function updateEnumIife(hostNode, iife, exportAssignment) {
    if (!ts.isParenthesizedExpression(iife.expression)
        || !ts.isFunctionExpression(iife.expression.expression)) {
        throw new Error('Invalid IIFE Structure');
    }
    // Ignore export assignment if variable is directly exported
    if (hostNode.modifiers
        && hostNode.modifiers.findIndex(m => m.kind == ts.SyntaxKind.ExportKeyword) != -1) {
        exportAssignment = undefined;
    }
    const expression = iife.expression.expression;
    const updatedFunction = ts.updateFunctionExpression(expression, expression.modifiers, expression.asteriskToken, expression.name, expression.typeParameters, expression.parameters, expression.type, ts.updateBlock(expression.body, [
        ...expression.body.statements,
        ts.createReturn(expression.parameters[0].name),
    ]));
    let arg = ts.createObjectLiteral();
    if (exportAssignment) {
        arg = ts.createBinary(exportAssignment, ts.SyntaxKind.BarBarToken, arg);
    }
    const updatedIife = ts.updateCall(iife, ts.updateParen(iife.expression, updatedFunction), iife.typeArguments, [arg]);
    let value = ast_utils_1.addPureComment(updatedIife);
    if (exportAssignment) {
        value = ts.createBinary(exportAssignment, ts.SyntaxKind.FirstAssignment, updatedIife);
    }
    return [updateHostNode(hostNode, value)];
}
function createWrappedEnum(name, hostNode, statements, literalInitializer = ts.createObjectLiteral(), addExportModifier = false) {
    const node = addExportModifier
        ? ts.updateVariableStatement(hostNode, [ts.createToken(ts.SyntaxKind.ExportKeyword)], hostNode.declarationList)
        : hostNode;
    const innerVarStmt = ts.createVariableStatement(undefined, ts.createVariableDeclarationList([
        ts.createVariableDeclaration(name, undefined, literalInitializer),
    ]));
    const innerReturn = ts.createReturn(ts.createIdentifier(name));
    const iife = ts.createImmediatelyInvokedFunctionExpression([
        innerVarStmt,
        ...statements,
        innerReturn,
    ]);
    return [updateHostNode(node, ast_utils_1.addPureComment(ts.createParen(iife)))];
}
function createWrappedClass(hostNode, statements) {
    const name = hostNode.name.text;
    const updatedStatements = [...statements];
    if (ts.isClassDeclaration(hostNode)) {
        updatedStatements[0] = ts.createClassDeclaration(hostNode.decorators, undefined, hostNode.name, hostNode.typeParameters, hostNode.heritageClauses, hostNode.members);
    }
    const pureIife = ast_utils_1.addPureComment(ts.createImmediatelyInvokedArrowFunction([
        ...updatedStatements,
        ts.createReturn(ts.createIdentifier(name)),
    ]));
    const modifiers = hostNode.modifiers;
    const isDefault = !!modifiers
        && modifiers.some(x => x.kind === ts.SyntaxKind.DefaultKeyword);
    const newStatement = [];
    newStatement.push(ts.createVariableStatement(isDefault ? undefined : modifiers, ts.createVariableDeclarationList([
        ts.createVariableDeclaration(name, undefined, pureIife),
    ], ts.NodeFlags.Let)));
    if (isDefault) {
        newStatement.push(ts.createExportAssignment(undefined, undefined, false, ts.createIdentifier(name)));
    }
    return newStatement;
}
