/** @import { Expression, ImportDeclaration, MemberExpression, Node, Program } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { build_getter, is_prop_source } from '../utils.js';
import * as b from '#compiler/builders';
import { add_state_transformers } from './shared/declarations.js';
import { transform_body } from '../../shared/transform-async.js';

/**
 * @param {Program} node
 * @param {ComponentContext} context
 */
export function Program(node, context) {
	if (!context.state.analysis.runes) {
		context.state.transform['$$props'] = {
			read: (node) => ({ ...node, name: '$$sanitized_props' })
		};

		for (const [name, binding] of context.state.scope.declarations) {
			if (binding.declaration_kind === 'import' && binding.mutated) {
				// the declaration itself is hoisted to the module scope, so we need
				// to resort to cruder measures to differentiate instance/module imports
				const { start, end } = context.state.analysis.instance.ast;
				const node = /** @type {ImportDeclaration} */ (binding.initial);
				const is_instance_import =
					/** @type {number} */ (node.start) > /** @type {number} */ (start) &&
					/** @type {number} */ (node.end) < /** @type {number} */ (end);

				if (is_instance_import) {
					const id = b.id('$$_import_' + name);

					context.state.transform[name] = {
						read: (_) => b.call(id),
						mutate: (_, mutation) => b.call(id, mutation)
					};

					context.state.legacy_reactive_imports.push(
						b.var(id, b.call('$.reactive_import', b.thunk(b.id(name))))
					);
				}
			}
		}
	}

	for (const [name, binding] of context.state.scope.declarations) {
		if (binding.kind === 'store_sub') {
			// read lazily, so that transforms added later are still applied
			/** @type {Expression} */
			let cached;

			const get_store = () => {
				return (cached ??= /** @type {Expression} */ (context.visit(b.id(name.slice(1)))));
			};

			context.state.transform[name] = {
				read: b.call,
				assign: (_, value) => b.call('$.store_set', get_store(), value),
				mutate: (node, mutation) => {
					// We need to untrack the store read, for consistency with Svelte 4
					const untracked = b.call('$.untrack', node);

					/**
					 *
					 * @param {Expression} n
					 * @returns {Expression}
					 */
					function replace(n) {
						if (n.type === 'MemberExpression') {
							return {
								...n,
								object: replace(/** @type {Expression} */ (n.object)),
								property: n.property
							};
						}

						return untracked;
					}

					return b.call(
						'$.store_mutate',
						get_store(),
						mutation.type === 'AssignmentExpression'
							? b.assignment(
									mutation.operator,
									/** @type {MemberExpression} */ (
										replace(/** @type {MemberExpression} */ (mutation.left))
									),
									mutation.right
								)
							: b.update(mutation.operator, replace(mutation.argument), mutation.prefix),
						untracked
					);
				},
				update: (node) => {
					return b.call(
						node.prefix ? '$.update_pre_store' : '$.update_store',
						build_getter(b.id(name.slice(1)), context.state),
						b.call(node.argument),
						node.operator === '--' && b.literal(-1)
					);
				}
			};
		}

		if (binding.kind === 'prop' || binding.kind === 'bindable_prop') {
			if (is_prop_source(binding, context.state)) {
				context.state.transform[name] = {
					read: b.call,
					assign: (node, value) => b.call(node, value),
					mutate: (node, value) => {
						if (binding.kind === 'bindable_prop') {
							// only necessary for interop with legacy parent bindings
							return b.call(node, value, b.true);
						}

						return value;
					},
					update: (node) => {
						return b.call(
							node.prefix ? '$.update_pre_prop' : '$.update_prop',
							node.argument,
							node.operator === '--' && b.literal(-1)
						);
					}
				};
			} else if (binding.prop_alias) {
				const key = b.key(binding.prop_alias);

				context.state.transform[name] = {
					read: (_) => b.member(b.id('$$props'), key, key.type === 'Literal')
				};
			} else {
				context.state.transform[name] = {
					read: (node) => b.member(b.id('$$props'), node)
				};
			}
		}
	}

	add_state_transformers(context);

	if (context.state.is_instance) {
		return {
			...node,
			body: transform_body(
				context.state.analysis.instance_body,
				b.id('$.run'),
				(node) => /** @type {Node} */ (context.visit(node))
			)
		};
	}

	context.next();
}
