"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.processScript = exports.isScriptProcessed = void 0;
const transform_1 = __importDefault(require("./transform"));
const instruction_1 = __importDefault(require("./instruction"));
const header_1 = require("./header");
const acorn_hammerhead_1 = require("acorn-hammerhead");
const esotope_hammerhead_1 = require("esotope-hammerhead");
const regexp_escape_1 = __importDefault(require("../../utils/regexp-escape"));
const get_bom_1 = __importDefault(require("../../utils/get-bom"));
const HTML_COMMENT_RE = /(^|\n)\s*<!--[^\n]*(\n|$)/g;
const OBJECT_RE = /^\s*\{.*\}\s*$/;
const TRAILING_SEMICOLON_RE = /;\s*$/;
const OBJECT_WRAPPER_RE = /^\s*\((.*)\);\s*$/;
const SOURCEMAP_RE = /(?:\/\/[@#][ \t]+sourceMappingURL=([^\s'"]+?)[ \t]*$)/gm;
const PROCESSED_SCRIPT_RE = new RegExp([
    (0, regexp_escape_1.default)(instruction_1.default.getLocation),
    (0, regexp_escape_1.default)(instruction_1.default.setLocation),
    (0, regexp_escape_1.default)(instruction_1.default.getProperty),
    (0, regexp_escape_1.default)(instruction_1.default.setProperty),
    (0, regexp_escape_1.default)(instruction_1.default.callMethod),
    (0, regexp_escape_1.default)(instruction_1.default.processScript),
    (0, regexp_escape_1.default)(instruction_1.default.processHtml),
    (0, regexp_escape_1.default)(instruction_1.default.getPostMessage),
    (0, regexp_escape_1.default)(instruction_1.default.getProxyUrl),
].join('|'));
const PARSING_OPTIONS = {
    allowReturnOutsideFunction: true,
    allowImportExportEverywhere: true,
    ecmaVersion: 13,
};
// Code pre/post-processing
function removeHtmlComments(code) {
    // NOTE: The JS parser removes the line that follows'<!--'. (T226589)
    do
        code = code.replace(HTML_COMMENT_RE, '\n');
    while (HTML_COMMENT_RE.test(code));
    return code;
}
function preprocess(code) {
    const bom = (0, get_bom_1.default)(code);
    let preprocessed = bom ? code.substring(bom.length) : code;
    preprocessed = (0, header_1.remove)(preprocessed);
    preprocessed = removeSourceMap(preprocessed);
    return { bom, preprocessed };
}
function removeSourceMap(code) {
    return code.replace(SOURCEMAP_RE, '');
}
function postprocess(processed, withHeader, bom, strictMode, swScopeHeaderValue, proxyless, workerSettings) {
    // NOTE: If the 'use strict' directive is not in the beginning of the file, it is ignored.
    // As we insert our header in the beginning of the script, we must put a new 'use strict'
    // before the header, otherwise it will be ignored.
    if (withHeader)
        processed = (0, header_1.add)(processed, strictMode, swScopeHeaderValue, proxyless, workerSettings);
    return bom ? bom + processed : processed;
}
// Parse/generate code
function removeTrailingSemicolon(processed, src) {
    return TRAILING_SEMICOLON_RE.test(src) ? processed : processed.replace(TRAILING_SEMICOLON_RE, '');
}
function getAst(src, isObject) {
    // NOTE: In case of objects (e.g.eval('{ 1: 2}')) without wrapping
    // object will be parsed as label. To avoid this we parenthesize src
    src = isObject ? `(${src})` : src;
    try {
        return (0, acorn_hammerhead_1.parse)(src, PARSING_OPTIONS);
    }
    catch (err) {
        return null;
    }
}
function getCode(ast, src) {
    const code = (0, esotope_hammerhead_1.generate)(ast, {
        format: {
            quotes: 'double',
            escapeless: true,
            compact: true,
        },
    });
    return src ? removeTrailingSemicolon(code, src) : code;
}
// Analyze code
function analyze(code) {
    let isObject = OBJECT_RE.test(code);
    let ast = getAst(code, isObject);
    // NOTE: `{ const a = 'foo'; }` edge case
    if (!ast && isObject) {
        ast = getAst(code, false);
        isObject = false;
    }
    return { ast, isObject };
}
function isArrayDataScript(ast) {
    const firstChild = ast.body[0];
    return ast.body.length === 1 &&
        firstChild.type === esotope_hammerhead_1.Syntax.ExpressionStatement &&
        firstChild.expression.type === esotope_hammerhead_1.Syntax.ArrayExpression;
}
function isStrictMode(ast) {
    if (ast.body.length) {
        const firstChild = ast.body[0];
        if (firstChild.type === esotope_hammerhead_1.Syntax.ExpressionStatement && firstChild.expression.type === esotope_hammerhead_1.Syntax.Literal)
            return firstChild.expression.value === 'use strict';
    }
    return false;
}
function applyChanges(script, changes, isObject) {
    const indexOffset = isObject ? -1 : 0;
    const chunks = [];
    let index = 0;
    if (!changes.length)
        return script;
    changes.sort((a, b) => (a.start - b.start) || (a.end - b.end) || // eslint-disable-line @typescript-eslint/no-extra-parens
        ((a.node.type === esotope_hammerhead_1.Syntax.VariableDeclaration ? 0 : 1) - (b.node.type === esotope_hammerhead_1.Syntax.VariableDeclaration ? 0 : 1))); // eslint-disable-line @typescript-eslint/no-extra-parens
    for (const change of changes) {
        const changeStart = change.start + indexOffset;
        const changeEnd = change.end + indexOffset;
        const parentheses = change.node.type === esotope_hammerhead_1.Syntax.SequenceExpression &&
            change.parentType !== esotope_hammerhead_1.Syntax.ExpressionStatement &&
            change.parentType !== esotope_hammerhead_1.Syntax.SequenceExpression;
        chunks.push(script.substring(index, changeStart));
        chunks.push(parentheses ? '(' : ' ');
        chunks.push(getCode(change.node, script.substring(changeStart, changeEnd)));
        chunks.push(parentheses ? ')' : ' ');
        index += changeEnd - index;
    }
    chunks.push(script.substring(index));
    return chunks.join('');
}
function isScriptProcessed(code) {
    return PROCESSED_SCRIPT_RE.test(code);
}
exports.isScriptProcessed = isScriptProcessed;
function processScript(src, withHeader = false, wrapLastExprWithProcessHtml = false, resolver, swScopeHeaderValue, proxyless, workerSettings) {
    const { bom, preprocessed } = preprocess(src);
    const withoutHtmlComments = removeHtmlComments(preprocessed);
    const { ast, isObject } = analyze(withoutHtmlComments);
    if (!ast)
        return src;
    withHeader = withHeader && !isObject && !isArrayDataScript(ast);
    const changes = proxyless ? [] : (0, transform_1.default)(ast, wrapLastExprWithProcessHtml, resolver);
    let processed = changes.length ? applyChanges(withoutHtmlComments, changes, isObject) : preprocessed;
    processed = postprocess(processed, withHeader, bom, isStrictMode(ast), swScopeHeaderValue, proxyless, workerSettings);
    if (isObject)
        processed = processed.replace(OBJECT_WRAPPER_RE, '$1');
    return processed;
}
exports.processScript = processScript;