/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types.js' */
import * as b from '#compiler/builders';
import {
	build_assignment_value,
	get_attribute_expression,
	is_event_attribute
} from '../../../../utils/ast.js';
import { dev, locate_node } from '../../../../state.js';
import { build_getter, should_proxy } from '../utils.js';
import { visit_assignment_expression } from '../../shared/assignments.js';
import { validate_mutation } from './shared/utils.js';
import { get_rune } from '../../../scope.js';
import { get_name } from '../../../nodes.js';

/**
 * @param {AssignmentExpression} node
 * @param {Context} context
 */
export function AssignmentExpression(node, context) {
	const expression = /** @type {Expression} */ (
		visit_assignment_expression(node, context, build_assignment) ?? context.next()
	);

	return validate_mutation(node, context, expression);
}

/**
 * Determines whether the value will be coerced on assignment (as with e.g. `+=`).
 * If not, we may need to proxify the value, or warn that the value will not be
 * proxified in time
 * @param {AssignmentOperator} operator
 */
function is_non_coercive_operator(operator) {
	return ['=', '||=', '&&=', '??='].includes(operator);
}

/** @type {Record<string, string>} */
const callees = {
	'=': '$.assign',
	'&&=': '$.assign_and',
	'||=': '$.assign_or',
	'??=': '$.assign_nullish'
};

/**
 * @param {AssignmentOperator} operator
 * @param {Pattern} left
 * @param {Expression} right
 * @param {Context} context
 * @returns {Expression | null}
 */
function build_assignment(operator, left, right, context) {
	if (context.state.analysis.runes && left.type === 'MemberExpression') {
		const name = get_name(left.property);
		const field = name && context.state.state_fields.get(name);

		if (field) {
			// special case — state declaration in class constructor
			if (field.node.type === 'AssignmentExpression' && left === field.node.left) {
				const rune = get_rune(right, context.state.scope);

				if (rune) {
					const child_state = {
						...context.state,
						in_constructor: rune !== '$derived' && rune !== '$derived.by'
					};

					let value = /** @type {Expression} */ (context.visit(right, child_state));

					if (dev) {
						const declaration = context.path.findLast(
							(parent) => parent.type === 'ClassDeclaration' || parent.type === 'ClassExpression'
						);
						value = b.call(
							'$.tag',
							value,
							b.literal(`${declaration?.id?.name ?? '[class]'}.${name}`)
						);
					}

					return b.assignment(operator, b.member(b.this, field.key), value);
				}
			}

			// special case — assignment to private state field
			if (left.property.type === 'PrivateIdentifier') {
				let value = /** @type {Expression} */ (
					context.visit(build_assignment_value(operator, left, right))
				);

				const needs_proxy =
					field.type === '$state' &&
					is_non_coercive_operator(operator) &&
					should_proxy(value, context.state.scope);

				return b.call('$.set', left, value, needs_proxy && b.true);
			}
		}
	}

	let object = left;

	while (object.type === 'MemberExpression') {
		// @ts-expect-error
		object = object.object;
	}

	if (object.type !== 'Identifier') {
		return null;
	}

	const binding = context.state.scope.get(object.name);
	if (!binding) return null;

	const transform = Object.hasOwn(context.state.transform, object.name)
		? context.state.transform[object.name]
		: null;

	const path = context.path.map((node) => node.type);

	// reassignment
	if (object === left && transform?.assign) {
		// special case — if an element binding, we know it's a primitive

		const is_primitive = path.at(-1) === 'BindDirective' && path.at(-2) === 'RegularElement';

		let value = /** @type {Expression} */ (
			context.visit(build_assignment_value(operator, left, right))
		);

		return transform.assign(
			object,
			value,
			!is_primitive &&
				binding.kind !== 'prop' &&
				binding.kind !== 'bindable_prop' &&
				binding.kind !== 'raw_state' &&
				binding.kind !== 'derived' &&
				binding.kind !== 'store_sub' &&
				context.state.analysis.runes &&
				should_proxy(right, context.state.scope) &&
				is_non_coercive_operator(operator)
		);
	}

	// mutation
	if (transform?.mutate) {
		let mutation = transform.mutate(
			object,
			b.assignment(
				operator,
				/** @type {Pattern} */ (context.visit(left)),
				/** @type {Expression} */ (context.visit(right))
			)
		);

		if (binding.legacy_indirect_bindings.size > 0) {
			mutation = b.sequence([
				mutation,
				b.call(
					'$.invalidate_inner_signals',
					b.arrow(
						[],
						b.block(
							Array.from(binding.legacy_indirect_bindings).map((binding) =>
								b.stmt(build_getter({ ...binding.node }, context.state))
							)
						)
					)
				)
			]);
		}

		return mutation;
	}

	// in cases like `(object.items ??= []).push(value)`, we may need to warn
	// if the value gets proxified, since the proxy _isn't_ the thing that
	// will be pushed to. we do this by transforming it to something like
	// `$.assign_nullish(object, 'items', [])`
	let should_transform =
		dev &&
		path.at(-1) !== 'ExpressionStatement' &&
		is_non_coercive_operator(operator) &&
		!context.state.scope.evaluate(right).is_primitive;

	// special case — ignore `onclick={() => (...)}`
	if (
		path.at(-1) === 'ArrowFunctionExpression' &&
		(path.at(-2) === 'RegularElement' || path.at(-2) === 'SvelteElement')
	) {
		const element = /** @type {AST.RegularElement} */ (context.path.at(-2));

		const attribute = element.attributes.find((attribute) => {
			if (attribute.type !== 'Attribute' || !is_event_attribute(attribute)) {
				return false;
			}

			const expression = get_attribute_expression(attribute);

			return expression === context.path.at(-1);
		});

		if (attribute) {
			should_transform = false;
		}
	}

	// special case — ignore `bind:prop={getter, (v) => (...)}` / `bind:value={x.y}`
	if (
		path.at(-1) === 'BindDirective' ||
		path.at(-1) === 'Component' ||
		path.at(-1) === 'SvelteComponent' ||
		(path.at(-1) === 'ArrowFunctionExpression' &&
			(path.at(-2) === 'BindDirective' ||
				(path.at(-2) === 'Component' && path.at(-3) === 'Fragment') ||
				(path.at(-2) === 'SequenceExpression' &&
					(path.at(-3) === 'Component' ||
						path.at(-3) === 'SvelteComponent' ||
						path.at(-3) === 'BindDirective'))))
	) {
		should_transform = false;
	}

	if (left.type === 'MemberExpression' && should_transform) {
		const callee = callees[operator];
		return /** @type {Expression} */ (
			context.visit(
				b.call(
					callee,
					/** @type {Expression} */ (left.object),
					/** @type {Expression} */ (
						left.computed
							? left.property
							: b.literal(/** @type {Identifier} */ (left.property).name)
					),
					right,
					b.literal(locate_node(left))
				)
			)
		);
	}

	return null;
}
