/** @import { AssignmentExpression, AssignmentOperator, Expression, Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Context, ServerTransformState } from '../types.js' */
import * as b from '#compiler/builders';
import { build_assignment_value } from '../../../../utils/ast.js';
import { get_name } from '../../../nodes.js';
import { get_rune } from '../../../scope.js';
import { visit_assignment_expression } from '../../shared/assignments.js';

/**
 * @param {AssignmentExpression} node
 * @param {Context} context
 */
export function AssignmentExpression(node, context) {
	return visit_assignment_expression(node, context, build_assignment) ?? context.next();
}

/**
 * Only returns an expression if this is not a `$store` assignment, as others can be kept as-is
 * @param {AssignmentOperator} operator
 * @param {Pattern} left
 * @param {Expression} right
 * @param {import('zimmerframe').Context<AST.SvelteNode, ServerTransformState>} context
 * @returns {Expression | null}
 */
function build_assignment(operator, left, right, context) {
	if (
		context.state.analysis.runes &&
		left.type === 'MemberExpression' &&
		left.object.type === 'ThisExpression' &&
		!left.computed
	) {
		const name = get_name(left.property);
		const field = name && context.state.state_fields.get(name);

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

			if (rune) {
				const key =
					left.property.type === 'PrivateIdentifier' || rune === '$state' || rune === '$state.raw'
						? left.property
						: field.key;

				return b.assignment(
					operator,
					b.member(b.this, key, key.type === 'Literal'),
					/** @type {Expression} */ (context.visit(right))
				);
			}
		} else if (
			field &&
			(field.type === '$derived' || field.type === '$derived.by') &&
			left.property.type === 'PrivateIdentifier'
		) {
			let value = /** @type {Expression} */ (
				context.visit(build_assignment_value(operator, left, right))
			);
			return b.call(b.member(b.this, name), value);
		}
	}

	let object = left;

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

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

	if (is_store_name(object.name)) {
		const name = object.name.slice(1);

		if (!context.state.scope.get(name)) {
			return null;
		}

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

			return b.call('$.store_set', b.id(name), value);
		}

		return b.call(
			'$.store_mutate',
			b.assignment('??=', b.id('$$store_subs'), b.object([])),
			b.literal(object.name),
			b.id(name),
			b.assignment(
				operator,
				/** @type {Pattern} */ (context.visit(left)),
				/** @type {Expression} */ (context.visit(right))
			)
		);
	}

	const binding = context.state.scope.get(object.name);

	// TODO 6.0 this won't work perfectly: once a derived is written to, it will
	// no longer recompute. It might be better to disallow writing to deriveds
	// on the server, to prevent this bug occurring
	if (binding?.kind === 'derived' && object === left) {
		let value = /** @type {Expression} */ (
			context.visit(build_assignment_value(operator, left, right))
		);

		return b.call(binding.node, value);
	}

	return null;
}

/**
 * @param {string} name
 */
function is_store_name(name) {
	return name[0] === '$' && /[A-Za-z_]/.test(name[1]);
}
