import { x } from 'code-red';
import { walk } from 'estree-walker';
import is_reference from 'is-reference';
import { clone } from '../../../utils/clone.js';
import flatten_reference from '../../utils/flatten_reference.js';
/**
 * @param {{
 * 	contexts: Context[];
 * 	node: import('estree').Pattern;
 * 	modifier?: DestructuredVariable['modifier'];
 * 	default_modifier?: DestructuredVariable['default_modifier'];
 * 	scope: import('./TemplateScope.js').default;
 * 	component: import('../../Component.js').default;
 * 	context_rest_properties: Map<string, import('estree').Node>;
 * 	in_rest_element?: boolean;
 * }} params
 */
export function unpack_destructuring({
	contexts,
	node,
	modifier = (node) => node,
	default_modifier = (node) => node,
	scope,
	component,
	context_rest_properties,
	in_rest_element = false
}) {
	if (!node) return;
	if (node.type === 'Identifier') {
		contexts.push({
			type: 'DestructuredVariable',
			key: /** @type {import('estree').Identifier} */ (node),
			modifier,
			default_modifier
		});
		if (in_rest_element) {
			context_rest_properties.set(node.name, node);
		}
		component.used_names.add(node.name);
	} else if (node.type === 'ArrayPattern') {
		node.elements.forEach((element, i) => {
			if (!element) {
				return;
			} else if (element.type === 'RestElement') {
				unpack_destructuring({
					contexts,
					node: element.argument,
					modifier: (node) =>
						/** @type {import('estree').Node} */ (x`${modifier(node)}.slice(${i})`),
					default_modifier,
					scope,
					component,
					context_rest_properties,
					in_rest_element: true
				});
			} else if (element.type === 'AssignmentPattern') {
				const n = contexts.length;
				mark_referenced(element.right, scope, component);
				unpack_destructuring({
					contexts,
					node: element.left,
					modifier: (node) => x`${modifier(node)}[${i}]`,
					default_modifier: (node, to_ctx) =>
						/** @type {import('estree').Node} */ (
							x`${node} !== undefined ? ${node} : ${update_reference(
								contexts,
								n,
								element.right,
								to_ctx
							)}`
						),
					scope,
					component,
					context_rest_properties,
					in_rest_element
				});
			} else {
				unpack_destructuring({
					contexts,
					node: element,
					modifier: (node) => /** @type {import('estree').Node} */ (x`${modifier(node)}[${i}]`),
					default_modifier,
					scope,
					component,
					context_rest_properties,
					in_rest_element
				});
			}
		});
	} else if (node.type === 'ObjectPattern') {
		const used_properties = [];
		node.properties.forEach((property) => {
			if (property.type === 'RestElement') {
				unpack_destructuring({
					contexts,
					node: property.argument,
					modifier: (node) =>
						/** @type {import('estree').Node} */ (
							x`@object_without_properties(${modifier(node)}, [${used_properties}])`
						),
					default_modifier,
					scope,
					component,
					context_rest_properties,
					in_rest_element: true
				});
			} else if (property.type === 'Property') {
				const key = property.key;
				const value = property.value;

				/** @type {(node: import('estree').Node) => import('estree').Node} */
				let new_modifier;
				if (property.computed) {
					// e.g { [computedProperty]: ... }
					const property_name = component.get_unique_name('computed_property');
					contexts.push({
						type: 'ComputedProperty',
						property_name,
						key
					});
					new_modifier = (node) => x`${modifier(node)}[${property_name}]`;
					used_properties.push(x`${property_name}`);
				} else if (key.type === 'Identifier') {
					// e.g. { someProperty: ... }
					const property_name = key.name;
					new_modifier = (node) => x`${modifier(node)}.${property_name}`;
					used_properties.push(x`"${property_name}"`);
				} else if (key.type === 'Literal') {
					// e.g. { "property-in-quotes": ... } or { 14: ... }
					const property_name = key.value;
					new_modifier = (node) => x`${modifier(node)}["${property_name}"]`;
					used_properties.push(x`"${property_name}"`);
				}
				if (value.type === 'AssignmentPattern') {
					// e.g. { property = default } or { property: newName = default }
					const n = contexts.length;
					mark_referenced(value.right, scope, component);
					unpack_destructuring({
						contexts,
						node: value.left,
						modifier: new_modifier,
						default_modifier: (node, to_ctx) =>
							/** @type {import('estree').Node} */ (
								x`${node} !== undefined ? ${node} : ${update_reference(
									contexts,
									n,
									value.right,
									to_ctx
								)}`
							),
						scope,
						component,
						context_rest_properties,
						in_rest_element
					});
				} else {
					// e.g. { property } or { property: newName }
					unpack_destructuring({
						contexts,
						node: value,
						modifier: new_modifier,
						default_modifier,
						scope,
						component,
						context_rest_properties,
						in_rest_element
					});
				}
			}
		});
	}
}

/**
 * @param {Context[]} contexts
 * @param {number} n
 * @param {import('estree').Expression} expression
 * @param {(name: string) => import('estree').Node} to_ctx
 * @returns {import('estree').Node}
 */
function update_reference(contexts, n, expression, to_ctx) {
	/** @param {import('estree').Identifier} node */
	const find_from_context = (node) => {
		for (let i = n; i < contexts.length; i++) {
			const cur_context = contexts[i];
			if (cur_context.type !== 'DestructuredVariable') continue;
			const { key } = cur_context;
			if (node.name === key.name) {
				throw new Error(`Cannot access '${node.name}' before initialization`);
			}
		}
		return to_ctx(node.name);
	};
	if (expression.type === 'Identifier') {
		return find_from_context(expression);
	}
	// NOTE: avoid unnecessary deep clone?
	expression = /** @type {import('estree').Expression} */ (clone(expression));
	walk(expression, {
		enter(node, parent) {
			if (
				is_reference(
					/** @type {import('is-reference').NodeWithPropertyDefinition} */ (node),
					/** @type {import('is-reference').NodeWithPropertyDefinition} */ (parent)
				)
			) {
				this.replace(find_from_context(/** @type {import('estree').Identifier} */ (node)));
				this.skip();
			}
		}
	});
	return expression;
}

/**
 * @param {import('estree').Node} node
 * @param {import('./TemplateScope.js').default} scope
 * @param {import('../../Component.js').default} component
 */
function mark_referenced(node, scope, component) {
	walk(node, {
		enter(node, parent) {
			if (is_reference(node, parent)) {
				const { name } = flatten_reference(node);
				if (!scope.is_let(name) && !scope.names.has(name)) {
					component.add_reference(node, name);
				}
			}
		}
	});
}

/** @typedef {DestructuredVariable | ComputedProperty} Context */

/**
 * @typedef {Object} ComputedProperty
 * @property {'ComputedProperty'} type
 * @property {import('estree').Identifier} property_name
 * @property {import('estree').Expression|import('estree').PrivateIdentifier} key
 */

/**
 * @typedef {Object} DestructuredVariable
 * @property {'DestructuredVariable'} type
 * @property {import('estree').Identifier} key
 * @property {string} [name]
 * @property {(node:import('estree').Node)=>import('estree').Node} modifier
 * @property {(node:import('estree').Node,to_ctx:(name:string)=>import('estree').Node)=>import('estree').Node} default_modifier
 */
