/** @import { CallExpression, Expression } from 'estree' */
/** @import { Context } from '../types' */
import { dev, is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
import { should_proxy } from '../utils.js';
import { get_inspect_args } from '../../utils.js';

/**
 * @param {CallExpression} node
 * @param {Context} context
 */
export function CallExpression(node, context) {
	const rune = get_rune(node, context.state.scope);

	switch (rune) {
		case '$host':
			return b.id('$$props.$$host');

		case '$effect.tracking':
			return b.call('$.effect_tracking');

		// transform state field assignments in constructors
		case '$state':
		case '$state.raw': {
			let arg = node.arguments[0];

			/** @type {Expression | undefined} */
			let value = undefined;

			if (arg) {
				value = /** @type {Expression} */ (context.visit(node.arguments[0]));

				if (
					rune === '$state' &&
					should_proxy(/** @type {Expression} */ (arg), context.state.scope)
				) {
					value = b.call('$.proxy', value);
				}
			}

			const callee = b.id('$.state', node.callee.loc);
			return b.call(callee, value);
		}

		case '$derived':
		case '$derived.by': {
			let fn = /** @type {Expression} */ (context.visit(node.arguments[0]));

			return b.call('$.derived', rune === '$derived' ? b.thunk(fn) : fn);
		}

		case '$state.eager':
			return b.call(
				'$.eager',
				b.thunk(/** @type {Expression} */ (context.visit(node.arguments[0])))
			);

		case '$state.snapshot':
			return b.call(
				'$.snapshot',
				/** @type {Expression} */ (context.visit(node.arguments[0])),
				is_ignored(node, 'state_snapshot_uncloneable') && b.true
			);

		case '$effect':
		case '$effect.pre': {
			const callee = rune === '$effect' ? '$.user_effect' : '$.user_pre_effect';
			const func = /** @type {Expression} */ (context.visit(node.arguments[0]));

			const expr = b.call(callee, /** @type {Expression} */ (func));
			expr.callee.loc = node.callee.loc; // ensure correct mapping

			return expr;
		}

		case '$effect.root':
			return b.call(
				'$.effect_root',
				.../** @type {Expression[]} */ (node.arguments.map((arg) => context.visit(arg)))
			);

		case '$effect.pending':
			return b.call('$.eager', b.thunk(b.call('$.pending')));

		case '$inspect':
		case '$inspect().with':
			return transform_inspect_rune(rune, node, context);
	}

	if (
		dev &&
		node.callee.type === 'MemberExpression' &&
		node.callee.object.type === 'Identifier' &&
		node.callee.object.name === 'console' &&
		context.state.scope.get('console') === null &&
		node.callee.property.type === 'Identifier' &&
		['debug', 'dir', 'error', 'group', 'groupCollapsed', 'info', 'log', 'trace', 'warn'].includes(
			node.callee.property.name
		) &&
		node.arguments.some(
			(arg) => arg.type === 'SpreadElement' || context.state.scope.evaluate(arg).has_unknown
		)
	) {
		return b.call(
			node.callee,
			b.spread(
				b.call(
					'$.log_if_contains_state',
					b.literal(node.callee.property.name),
					.../** @type {Expression[]} */ (node.arguments.map((arg) => context.visit(arg)))
				)
			)
		);
	}

	context.next();
}

/**
 * @param {'$inspect' | '$inspect().with'} rune
 * @param {CallExpression} node
 * @param {Context} context
 */
function transform_inspect_rune(rune, node, context) {
	if (!dev) return b.empty;

	const { args, inspector } = get_inspect_args(rune, node, context.visit);

	// by passing an arrow function, the log appears to come from the `$inspect` callsite
	// rather than the `inspect.js` file containing the utility
	const id = b.id('$$args');
	const fn = b.arrow([b.rest(id)], b.call(inspector, b.spread(id)));

	return b.call('$.inspect', b.thunk(b.array(args)), fn, rune === '$inspect' && b.true);
}
