"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.transformYieldExpression = exports.transformFunctionDeclaration = exports.transformFunctionLikeDeclaration = exports.transformFunctionToExpression = exports.transformParameters = exports.transformFunctionBody = exports.transformFunctionBodyHeader = exports.transformFunctionBodyContent = void 0;
const ts = require("typescript");
const lua = require("../../LuaAST");
const utils_1 = require("../../utils");
const annotations_1 = require("../utils/annotations");
const diagnostics_1 = require("../utils/diagnostics");
const export_1 = require("../utils/export");
const function_context_1 = require("../utils/function-context");
const lua_ast_1 = require("../utils/lua-ast");
const lualib_1 = require("../utils/lualib");
const scope_1 = require("../utils/scope");
const async_await_1 = require("./async-await");
const identifier_1 = require("./identifier");
const return_1 = require("./return");
const variable_declaration_1 = require("./variable-declaration");
function transformParameterDefaultValueDeclaration(context, parameterName, value, tsOriginal) {
    const parameterValue = value ? context.transformExpression(value) : undefined;
    const assignment = lua.createAssignmentStatement(parameterName, parameterValue);
    const nilCondition = lua.createBinaryExpression(parameterName, lua.createNilLiteral(), lua.SyntaxKind.EqualityOperator);
    const ifBlock = lua.createBlock([assignment]);
    return lua.createIfStatement(nilCondition, ifBlock, undefined, tsOriginal);
}
function isRestParameterReferenced(identifier, scope) {
    if (!identifier.symbolId) {
        return true;
    }
    if (scope.referencedSymbols === undefined) {
        return false;
    }
    const references = scope.referencedSymbols.get(identifier.symbolId);
    return references !== undefined && references.length > 0;
}
function transformFunctionBodyContent(context, body) {
    if (!ts.isBlock(body)) {
        const returnStatement = return_1.transformExpressionBodyToReturnStatement(context, body);
        return [returnStatement];
    }
    const bodyStatements = scope_1.performHoisting(context, context.transformStatements(body.statements));
    return bodyStatements;
}
exports.transformFunctionBodyContent = transformFunctionBodyContent;
function transformFunctionBodyHeader(context, bodyScope, parameters, spreadIdentifier) {
    const headerStatements = [];
    // Add default parameters and object binding patterns
    const bindingPatternDeclarations = [];
    let bindPatternIndex = 0;
    for (const declaration of parameters) {
        if (ts.isObjectBindingPattern(declaration.name) || ts.isArrayBindingPattern(declaration.name)) {
            const identifier = lua.createIdentifier(`____bindingPattern${bindPatternIndex++}`);
            if (declaration.initializer !== undefined) {
                // Default binding parameter
                headerStatements.push(transformParameterDefaultValueDeclaration(context, identifier, declaration.initializer));
            }
            // Binding pattern
            bindingPatternDeclarations.push(...variable_declaration_1.transformBindingPattern(context, declaration.name, identifier));
        }
        else if (declaration.initializer !== undefined) {
            // Default parameter
            headerStatements.push(transformParameterDefaultValueDeclaration(context, identifier_1.transformIdentifier(context, declaration.name), declaration.initializer));
        }
    }
    // Push spread operator here
    if (spreadIdentifier && isRestParameterReferenced(spreadIdentifier, bodyScope)) {
        const spreadTable = lua_ast_1.wrapInTable(lua.createDotsLiteral());
        headerStatements.push(lua.createVariableDeclarationStatement(spreadIdentifier, spreadTable));
    }
    // Binding pattern statements need to be after spread table is declared
    headerStatements.push(...bindingPatternDeclarations);
    return headerStatements;
}
exports.transformFunctionBodyHeader = transformFunctionBodyHeader;
function transformFunctionBody(context, parameters, body, spreadIdentifier, node) {
    const scope = scope_1.pushScope(context, scope_1.ScopeType.Function);
    scope.node = node;
    let bodyStatements = transformFunctionBodyContent(context, body);
    if (node && async_await_1.isAsyncFunction(node)) {
        bodyStatements = async_await_1.wrapInAsyncAwaiter(context, bodyStatements);
    }
    const headerStatements = transformFunctionBodyHeader(context, scope, parameters, spreadIdentifier);
    scope_1.popScope(context);
    return [[...headerStatements, ...bodyStatements], scope];
}
exports.transformFunctionBody = transformFunctionBody;
function transformParameters(context, parameters, functionContext) {
    // Build parameter string
    const paramNames = [];
    if (functionContext) {
        paramNames.push(functionContext);
    }
    let restParamName;
    let dotsLiteral;
    let identifierIndex = 0;
    // Only push parameter name to paramName array if it isn't a spread parameter
    for (const param of parameters) {
        if (ts.isIdentifier(param.name) && param.name.originalKeywordKind === ts.SyntaxKind.ThisKeyword) {
            continue;
        }
        // Binding patterns become ____bindingPattern0, ____bindingPattern1, etc as function parameters
        // See transformFunctionBody for how these values are destructured
        const paramName = ts.isObjectBindingPattern(param.name) || ts.isArrayBindingPattern(param.name)
            ? lua.createIdentifier(`____bindingPattern${identifierIndex++}`)
            : identifier_1.transformIdentifier(context, param.name);
        // This parameter is a spread parameter (...param)
        if (!param.dotDotDotToken) {
            paramNames.push(paramName);
        }
        else {
            restParamName = paramName;
            // Push the spread operator into the paramNames array
            dotsLiteral = lua.createDotsLiteral();
        }
    }
    return [paramNames, dotsLiteral, restParamName];
}
exports.transformParameters = transformParameters;
function transformFunctionToExpression(context, node) {
    utils_1.assert(node.body);
    const type = context.checker.getTypeAtLocation(node);
    let functionContext;
    if (function_context_1.getFunctionContextType(context, type) !== function_context_1.ContextType.Void) {
        if (ts.isArrowFunction(node)) {
            // dummy context for arrow functions with parameters
            if (node.parameters.length > 0) {
                functionContext = lua.createAnonymousIdentifier();
            }
        }
        else {
            // self context
            functionContext = lua_ast_1.createSelfIdentifier();
        }
    }
    let flags = lua.FunctionExpressionFlags.None;
    if (!ts.isBlock(node.body))
        flags |= lua.FunctionExpressionFlags.Inline;
    if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) {
        flags |= lua.FunctionExpressionFlags.Declaration;
    }
    const [paramNames, dotsLiteral, spreadIdentifier] = transformParameters(context, node.parameters, functionContext);
    const [transformedBody, functionScope] = transformFunctionBody(context, node.parameters, node.body, spreadIdentifier, node);
    const functionExpression = lua.createFunctionExpression(lua.createBlock(transformedBody), paramNames, dotsLiteral, flags, node);
    return [
        node.asteriskToken
            ? lualib_1.transformLuaLibFunction(context, lualib_1.LuaLibFeature.Generator, undefined, functionExpression)
            : functionExpression,
        functionScope,
    ];
}
exports.transformFunctionToExpression = transformFunctionToExpression;
function transformFunctionLikeDeclaration(node, context) {
    if (node.body === undefined) {
        // This code can be reached only from object methods, which is TypeScript error
        return lua.createNilLiteral();
    }
    if (annotations_1.getNodeAnnotations(node).has(annotations_1.AnnotationKind.TupleReturn)) {
        context.diagnostics.push(diagnostics_1.annotationRemoved(node, annotations_1.AnnotationKind.TupleReturn));
    }
    const [functionExpression, functionScope] = transformFunctionToExpression(context, node);
    // Handle named function expressions which reference themselves
    if (ts.isFunctionExpression(node) && node.name && functionScope.referencedSymbols) {
        const symbol = context.checker.getSymbolAtLocation(node.name);
        if (symbol) {
            // TODO: Not using symbol ids because of https://github.com/microsoft/TypeScript/issues/37131
            const isReferenced = [...functionScope.referencedSymbols].some(([, nodes]) => nodes.some(n => { var _a; return ((_a = context.checker.getSymbolAtLocation(n)) === null || _a === void 0 ? void 0 : _a.valueDeclaration) === symbol.valueDeclaration; }));
            // Only wrap if the name is actually referenced inside the function
            if (isReferenced) {
                const nameIdentifier = identifier_1.transformIdentifier(context, node.name);
                // We cannot use transformToImmediatelyInvokedFunctionExpression() here because we need to transpile
                // the function first to determine if it's self-referencing. Fortunately, this does not cause issues
                // with var-arg optimization because the IIFE is just wrapping another function which will already push
                // another scope.
                return lua_ast_1.createImmediatelyInvokedFunctionExpression([lua.createVariableDeclarationStatement(nameIdentifier, functionExpression)], lua.cloneIdentifier(nameIdentifier));
            }
        }
    }
    return functionExpression;
}
exports.transformFunctionLikeDeclaration = transformFunctionLikeDeclaration;
const transformFunctionDeclaration = (node, context) => {
    var _a;
    if (annotations_1.getNodeAnnotations(node).has(annotations_1.AnnotationKind.TupleReturn)) {
        context.diagnostics.push(diagnostics_1.annotationRemoved(node, annotations_1.AnnotationKind.TupleReturn));
    }
    // Don't transform functions without body (overload declarations)
    if (node.body === undefined) {
        return undefined;
    }
    if (export_1.hasDefaultExportModifier(node)) {
        return lua.createAssignmentStatement(lua.createTableIndexExpression(lua_ast_1.createExportsIdentifier(), export_1.createDefaultExportStringLiteral(node)), transformFunctionLikeDeclaration(node, context));
    }
    const [functionExpression, functionScope] = transformFunctionToExpression(context, node);
    // Name being undefined without default export is a TypeScript error
    const name = node.name ? identifier_1.transformIdentifier(context, node.name) : lua.createAnonymousIdentifier();
    // Remember symbols referenced in this function for hoisting later
    if (name.symbolId !== undefined) {
        const scope = scope_1.peekScope(context);
        if (!scope.functionDefinitions) {
            scope.functionDefinitions = new Map();
        }
        const functionInfo = { referencedSymbols: (_a = functionScope.referencedSymbols) !== null && _a !== void 0 ? _a : new Map() };
        scope.functionDefinitions.set(name.symbolId, functionInfo);
    }
    return lua_ast_1.createLocalOrExportedOrGlobalDeclaration(context, name, functionExpression, node);
};
exports.transformFunctionDeclaration = transformFunctionDeclaration;
const transformYieldExpression = (expression, context) => {
    const parameters = expression.expression ? [context.transformExpression(expression.expression)] : [];
    return expression.asteriskToken
        ? lualib_1.transformLuaLibFunction(context, lualib_1.LuaLibFeature.DelegatedYield, expression, ...parameters)
        : lua.createCallExpression(lua.createTableIndexExpression(lua.createIdentifier("coroutine"), lua.createStringLiteral("yield")), parameters, expression);
};
exports.transformYieldExpression = transformYieldExpression;
//# sourceMappingURL=function.js.map