import { nodes_match } from '../../utils/nodes_match.js';
import { x } from 'code-red';

/**
 * @param {import('./Renderer.js').default} renderer
 * @param {import('periscopic').Scope} scope
 * @param {import('estree').Node} node
 * @param {Set<string>} names
 * @param {boolean} main_execution_context
 * @returns {any}
 */
export function invalidate(renderer, scope, node, names, main_execution_context = false) {
	const { component } = renderer;
	const [head, ...tail] = /** @type {import('../../interfaces.js').Var[]} */ (
		Array.from(names)
			.filter((name) => {
				const owner = scope.find_owner(name);
				return !owner || owner === component.instance_scope;
			})
			.map((name) => component.var_lookup.get(name))
			.filter((variable) => {
				return (
					variable &&
					!variable.hoistable &&
					!variable.global &&
					!variable.module &&
					(variable.referenced ||
						variable.subscribable ||
						variable.is_reactive_dependency ||
						variable.export_name ||
						variable.name[0] === '$')
				);
			})
	);

	/**
	 * @param {import('../../interfaces.js').Var} variable
	 * @param {import('estree').Expression} [node]
	 */
	function get_invalidated(variable, node) {
		if (main_execution_context && !variable.subscribable && variable.name[0] !== '$') {
			return node;
		}
		return renderer_invalidate(renderer, variable.name, undefined, main_execution_context);
	}
	if (!head) {
		return node;
	}
	component.has_reactive_assignments = true;
	if (
		node.type === 'AssignmentExpression' &&
		node.operator === '=' &&
		nodes_match(node.left, node.right) &&
		tail.length === 0
	) {
		return get_invalidated(head, node);
	}
	const is_store_value = head.name[0] === '$' && head.name[1] !== '$';
	const extra_args = tail.map((variable) => get_invalidated(variable)).filter(Boolean);
	if (is_store_value) {
		return x`@set_store_value(${head.name.slice(1)}, ${node}, ${head.name}, ${extra_args})`;
	}

	let invalidate;
	if (!main_execution_context) {
		const pass_value =
			extra_args.length > 0 ||
			(node.type === 'AssignmentExpression' && node.left.type !== 'Identifier') ||
			(node.type === 'UpdateExpression' && (!node.prefix || node.argument.type !== 'Identifier'));
		if (pass_value) {
			extra_args.unshift({
				type: 'Identifier',
				name: head.name
			});
		}
		invalidate = x`$$invalidate(${
			renderer.context_lookup.get(head.name).index
		}, ${node}, ${extra_args})`;
	} else {
		// skip `$$invalidate` if it is in the main execution context
		invalidate = extra_args.length ? [node, ...extra_args] : node;
	}
	if (head.subscribable && head.reassigned) {
		const subscribe = `$$subscribe_${head.name}`;
		invalidate = x`${subscribe}(${invalidate})`;
	}
	return invalidate;
}

/**
 * @param {import('./Renderer.js').default} renderer
 * @param {string} name
 * @param {any} [value]
 * @param {boolean} [main_execution_context]
 * @returns {import('estree').Node}
 */
export function renderer_invalidate(renderer, name, value, main_execution_context = false) {
	const variable = renderer.component.var_lookup.get(name);
	if (variable && variable.subscribable && (variable.reassigned || variable.export_name)) {
		if (main_execution_context) {
			return x`${`$$subscribe_${name}`}(${value || name})`;
		} else {
			const member = renderer.context_lookup.get(name);
			return x`${`$$subscribe_${name}`}($$invalidate(${member.index}, ${value || name}))`;
		}
	}
	if (name[0] === '$' && name[1] !== '$') {
		return x`${name.slice(1)}.set(${value || name})`;
	}
	if (
		variable &&
		(variable.module ||
			(!variable.referenced &&
				!variable.is_reactive_dependency &&
				!variable.export_name &&
				!name.startsWith('$$')))
	) {
		return value || name;
	}
	if (value) {
		if (main_execution_context) {
			return x`${value}`;
		} else {
			const member = renderer.context_lookup.get(name);
			return x`$$invalidate(${member.index}, ${value})`;
		}
	}
	if (main_execution_context) return;
	// if this is a reactive declaration, invalidate dependencies recursively
	const deps = new Set([name]);
	deps.forEach((name) => {
		const reactive_declarations = renderer.component.reactive_declarations.filter((x) =>
			x.assignees.has(name)
		);
		reactive_declarations.forEach((declaration) => {
			declaration.dependencies.forEach((name) => {
				deps.add(name);
			});
		});
	});
	// TODO ideally globals etc wouldn't be here in the first place
	const filtered = Array.from(deps).filter((n) => renderer.context_lookup.has(n));
	if (!filtered.length) return null;
	return filtered
		.map((n) => x`$$invalidate(${renderer.context_lookup.get(n).index}, ${n})`)
		.reduce((lhs, rhs) => x`${lhs}, ${rhs}`);
}
