
const {Parser: AcornParser, isNewLine: acornIsNewLine, getLineInfo: acornGetLineInfo} = require('acorn');
const {full: acornWalkFull} = require('acorn-walk');

const INTERNAL_STATE_NAME = 'VM2_INTERNAL_STATE_DO_NOT_USE_OR_PROGRAM_WILL_FAIL';

function assertType(node, type) {
	if (!node) throw new Error(`None existent node expected '${type}'`);
	if (node.type !== type) throw new Error(`Invalid node type '${node.type}' expected '${type}'`);
	return node;
}

function makeNiceSyntaxError(message, code, filename, location, tokenizer) {
	const loc = acornGetLineInfo(code, location);
	let end = location;
	while (end < code.length && !acornIsNewLine(code.charCodeAt(end))) {
		end++;
	}
	let markerEnd = tokenizer.start === location ? tokenizer.end : location + 1;
	if (!markerEnd || markerEnd > end) markerEnd = end;
	let markerLen = markerEnd - location;
	if (markerLen <= 0) markerLen = 1;
	if (message === 'Unexpected token') {
		const type = tokenizer.type;
		if (type.label === 'name' || type.label === 'privateId') {
			message = 'Unexpected identifier';
		} else if (type.label === 'eof') {
			message = 'Unexpected end of input';
		} else if (type.label === 'num') {
			message = 'Unexpected number';
		} else if (type.label === 'string') {
			message = 'Unexpected string';
		} else if (type.label === 'regexp') {
			message = 'Unexpected token \'/\'';
			markerLen = 1;
		} else {
			const token = tokenizer.value || type.label;
			message = `Unexpected token '${token}'`;
		}
	}
	const error = new SyntaxError(message);
	if (!filename) return error;
	const line = code.slice(location - loc.column, end);
	const marker = line.slice(0, loc.column).replace(/\S/g, ' ') + '^'.repeat(markerLen);
	error.stack = `${filename}:${loc.line}\n${line}\n${marker}\n\n${error.stack}`;
	return error;
}

function transformer(args, body, isAsync, isGenerator, filename) {
	let code;
	let argsOffset;
	if (args === null) {
		code = body;
		// SECURITY (GHSA-wp5r-2gw5-m7q7): the fast-path skip avoids the AST
		// walk for code that contains none of the keywords whose presence
		// requires instrumentation. Pre-fix the regex tested only `catch`,
		// `import`, `async` — but the AST walker also instruments `with`
		// statements (wraps the head in `wrapWith()` to enforce the
		// `INTERNAL_STATE_NAME` unscopable invariant) and rejects the
		// `INTERNAL_STATE_NAME` identifier outright. Code that contained
		// neither `catch`/`import`/`async` could therefore reach
		// `VM2_INTERNAL_STATE_DO_NOT_USE_OR_PROGRAM_WILL_FAIL` and use a
		// raw `with()` statement, bypassing both controls. Add `with` to
		// the keyword set, and additionally substring-test the entire
		// source for `INTERNAL_STATE_NAME` so the identifier check cannot
		// be skipped. Keywords cannot contain `u`-escape sequences in
		// JavaScript source, so a simple regex over `code` is sufficient.
		// Defensive: a custom compiler may return undefined (existing test
		// "modules > optionally can run a custom compiler function" exercises
		// this). Pre-fix the regex.test below silently coerced undefined to
		// "undefined" and returned false; the new substring check would
		// throw. Coerce to a defined string for both checks.
		//
		// SECURITY (post-GHSA-wp5r-2gw5-m7q7 hardening): identifiers in
		// JavaScript CAN contain `\uXXXX` / `\u{...}` unicode escapes —
		// `VM2_INTERNAL_STATE_DO_NOT_USE_OR_PROGRAM_WILL_FAIL` is a
		// valid identifier reference for the same global. The `indexOf`
		// substring check above only matches the raw form, so bypassed.
		// Force fall-through to AST when the source contains any `\u`
		// escape; the AST walker decodes escapes and inspects the actual
		// identifier name.
		const codeStr = typeof code === 'string' ? code : '';
		if (
			!/\b(?:catch|import|async|with)\b/.test(codeStr) &&
			codeStr.indexOf(INTERNAL_STATE_NAME) === -1 &&
			codeStr.indexOf('\\u') === -1
		) {
			return {__proto__: null, code, hasAsync: false};
		}
	} else {
		code = isAsync ? '(async function' : '(function';
		if (isGenerator) code += '*';
		code += ' anonymous(';
		code += args;
		argsOffset = code.length;
		code += '\n) {\n';
		code += body;
		code += '\n})';
	}

	const parser = new AcornParser({
		__proto__: null,
		ecmaVersion: 2022,
		allowAwaitOutsideFunction: args === null && isAsync,
		allowReturnOutsideFunction: args === null
	}, code);
	let ast;
	try {
		ast = parser.parse();
	} catch (e) {
		// Try to generate a nicer error message.
		if (e instanceof SyntaxError && e.pos !== undefined) {
			let message = e.message;
			const match = message.match(/^(.*) \(\d+:\d+\)$/);
			if (match) message = match[1];
			e = makeNiceSyntaxError(message, code, filename, e.pos, parser);
		}
		throw e;
	}

	if (args !== null) {
		const pBody = assertType(ast, 'Program').body;
		if (pBody.length !== 1) throw new SyntaxError('Single function literal required');
		const expr = pBody[0];
		if (expr.type !== 'ExpressionStatement') throw new SyntaxError('Single function literal required');
		const func = expr.expression;
		if (func.type !== 'FunctionExpression') throw new SyntaxError('Single function literal required');
		if (func.body.start !== argsOffset + 3) throw new SyntaxError('Unexpected end of arg string');
	}

	const insertions = [];
	let hasAsync = false;

	const TO_LEFT = -100;
	const TO_RIGHT = 100;

	let internStateValiable = undefined;
	let tmpname = 'VM2_INTERNAL_TMPNAME';

	acornWalkFull(ast, (node, state, type) => {
		if (type === 'Function') {
			if (node.async) hasAsync = true;
		}
		const nodeType = node.type;
		if (nodeType === 'CatchClause') {
			const param = node.param;
			if (param) {
				if (param.type === 'Identifier') {
					const name = assertType(param, 'Identifier').name;
					const cBody = assertType(node.body, 'BlockStatement');
					if (cBody.body.length > 0) {
						insertions.push({
							__proto__: null,
							pos: cBody.body[0].start,
							order: TO_LEFT,
							coder: () => `${name}=${INTERNAL_STATE_NAME}.handleException(${name});`
						});
					}
				} else {
					insertions.push({
						__proto__: null,
						pos: node.start,
						order: TO_RIGHT,
						coder: () => `catch(${tmpname}){${tmpname}=${INTERNAL_STATE_NAME}.handleException(${tmpname});try{throw ${tmpname};}`
					});
					insertions.push({
						__proto__: null,
						pos: node.body.end,
						order: TO_LEFT,
						coder: () => `}`
					});
				}
			}
		} else if (nodeType === 'WithStatement') {
			insertions.push({
				__proto__: null,
				pos: node.object.start,
				order: TO_LEFT,
				coder: () => INTERNAL_STATE_NAME + '.wrapWith('
			});
			insertions.push({
				__proto__: null,
				pos: node.object.end,
				order: TO_RIGHT,
				coder: () => ')'
			});
		} else if (nodeType === 'Identifier') {
			if (node.name === INTERNAL_STATE_NAME) {
				if (internStateValiable === undefined || internStateValiable.start > node.start) {
					internStateValiable = node;
				}
			} else if (node.name.startsWith(tmpname)) {
				tmpname = node.name + '_UNIQUE';
			}
		} else if (nodeType === 'ImportExpression') {
			insertions.push({
				__proto__: null,
				pos: node.start,
				order: TO_RIGHT,
				coder: () => INTERNAL_STATE_NAME + '.'
			});
		}
	});

	if (internStateValiable) {
		throw makeNiceSyntaxError('Use of internal vm2 state variable', code, filename, internStateValiable.start, {
			__proto__: null,
			start: internStateValiable.start,
			end: internStateValiable.end
		});
	}

	if (insertions.length === 0) return {__proto__: null, code, hasAsync};

	insertions.sort((a, b) => (a.pos == b.pos ? a.order - b.order : a.pos - b.pos));

	let ncode = '';
	let curr = 0;
	for (let i = 0; i < insertions.length; i++) {
		const change = insertions[i];
		ncode += code.substring(curr, change.pos) + change.coder();
		curr = change.pos;
	}
	ncode += code.substring(curr);

	return {__proto__: null, code: ncode, hasAsync};
}

exports.INTERNAL_STATE_NAME = INTERNAL_STATE_NAME;
exports.transformer = transformer;
