import { b } from 'code-red';
import { string_literal } from '../utils/stringify.js';
import Renderer from './Renderer.js';
import { extract_names } from 'periscopic';
import { walk } from 'estree-walker';
import { invalidate } from '../render_dom/invalidate.js';
import check_enable_sourcemap from '../utils/check_enable_sourcemap.js';

/**
 * @param {import('../Component.js').default} component
 * @param {import('../../interfaces.js').CompileOptions} options
 * @returns {{ js: import('estree').Node[]; css: import('../../interfaces.js').CssResult; }}
 */
export default function ssr(component, options) {
	const renderer = new Renderer({
		name: component.name
	});
	const { name } = component;
	// create $$render function
	renderer.render(
		trim(component.fragment.children),
		Object.assign(
			{
				locate: component.locate
			},
			options
		)
	);
	// TODO put this inside the Renderer class
	const literal = renderer.pop();
	// TODO concatenate CSS maps
	const css = options.customElement
		? { code: null, map: null }
		: component.stylesheet.render(options.filename);
	const uses_rest = component.var_lookup.has('$$restProps');
	const props = component.vars.filter((variable) => !variable.module && variable.export_name);
	const rest = uses_rest
		? b`let $$restProps = @compute_rest_props($$props, [${props
				.map((prop) => `"${prop.export_name}"`)
				.join(',')}]);`
		: null;
	const uses_slots = component.var_lookup.has('$$slots');
	const slots = uses_slots ? b`let $$slots = @compute_slots(#slots);` : null;
	const reactive_stores = component.vars.filter(
		(variable) => variable.name[0] === '$' && variable.name[1] !== '$'
	);
	const reactive_store_subscriptions = reactive_stores
		.filter((store) => {
			const variable = component.var_lookup.get(store.name.slice(1));
			return !variable || variable.hoistable;
		})
		.map(({ name }) => {
			const store_name = name.slice(1);
			return b`
				${component.compile_options.dev && b`@validate_store(${store_name}, '${store_name}');`}
				${`$$unsubscribe_${store_name}`} = @subscribe(${store_name}, #value => ${name} = #value)
			`;
		});
	const reactive_store_unsubscriptions = reactive_stores.map(
		({ name }) => b`${`$$unsubscribe_${name.slice(1)}`}()`
	);
	const reactive_store_declarations = reactive_stores.map(({ name }) => {
		const store_name = name.slice(1);
		const store = component.var_lookup.get(store_name);
		if (store && store.reassigned) {
			const unsubscribe = `$$unsubscribe_${store_name}`;
			const subscribe = `$$subscribe_${store_name}`;
			return b`let ${name}, ${unsubscribe} = @noop, ${subscribe} = () => (${unsubscribe}(), ${unsubscribe} = @subscribe(${store_name}, $$value => ${name} = $$value), ${store_name})`;
		}
		return b`let ${name}, ${`$$unsubscribe_${store_name}`};`;
	});
	// instrument get/set store value
	if (component.ast.instance) {
		let scope = component.instance_scope;
		const map = component.instance_scope_map;
		walk(component.ast.instance.content, {
			enter(node) {
				if (map.has(node)) {
					scope = map.get(node);
				}
			},
			leave(node) {
				if (map.has(node)) {
					scope = scope.parent;
				}
				if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') {
					const assignee = node.type === 'AssignmentExpression' ? node.left : node.argument;
					const names = new Set(extract_names(/** @type {import('estree').Node} */ (assignee)));
					const to_invalidate = new Set();
					for (const name of names) {
						const variable = component.var_lookup.get(name);
						if (
							variable &&
							!variable.hoistable &&
							!variable.global &&
							!variable.module &&
							(variable.subscribable || variable.name[0] === '$')
						) {
							to_invalidate.add(variable.name);
						}
					}
					if (to_invalidate.size) {
						this.replace(
							invalidate(/** @type {any} */ ({ component }), scope, node, to_invalidate, true)
						);
					}
				}
			}
		});
	}
	component.rewrite_props(({ name, reassigned }) => {
		const value = `$${name}`;
		let insert = reassigned
			? b`${`$$subscribe_${name}`}()`
			: b`${`$$unsubscribe_${name}`} = @subscribe(${name}, #value => $${value} = #value)`;
		if (component.compile_options.dev) {
			insert = b`@validate_store(${name}, '${name}'); ${insert}`;
		}
		return insert;
	});
	const instance_javascript = component.extract_javascript(component.ast.instance);
	// TODO only do this for props with a default value
	const parent_bindings = instance_javascript
		? component.vars
				.filter((variable) => !variable.module && variable.export_name)
				.map((prop) => {
					return b`if ($$props.${prop.export_name} === void 0 && $$bindings.${prop.export_name} && ${prop.name} !== void 0) $$bindings.${prop.export_name}(${prop.name});`;
				})
		: [];
	const injected = Array.from(component.injected_reactive_declaration_vars).filter((name) => {
		const variable = component.var_lookup.get(name);
		return variable.injected;
	});
	const reactive_declarations = component.reactive_declarations.map((d) => {
		const body = /** @type {import('estree').LabeledStatement} */ (d.node).body;
		let statement = b`${body}`;
		if (!d.declaration) {
			// TODO do not add label if it's not referenced
			statement = b`$: { ${statement} }`;
		}
		return statement;
	});
	const main = renderer.has_bindings
		? b`
			let $$settled;
			let $$rendered;
			let #previous_head = $$result.head;

			do {
				$$settled = true;
				// $$result.head is mutated by the literal expression
				// need to reset it if we're looping back to prevent duplication
				$$result.head = #previous_head;

				${reactive_declarations}

				$$rendered = ${literal};
			} while (!$$settled);

			${reactive_store_unsubscriptions}

			return $$rendered;
		`
		: b`
			${reactive_declarations}

			${reactive_store_unsubscriptions}

			return ${literal};`;
	const blocks = [
		...injected.map((name) => b`let ${name};`),
		rest,
		slots,
		...reactive_store_declarations,
		...reactive_store_subscriptions,
		instance_javascript,
		...parent_bindings,
		css.code && b`$$result.css.add(#css);`,
		main
	].filter(Boolean);
	const css_sourcemap_enabled = check_enable_sourcemap(options.enableSourcemap, 'css');
	const js = b`
		${
			css.code
				? b`
		const #css = {
			code: "${css.code}",
			map: ${css_sourcemap_enabled && css.map ? string_literal(css.map.toString()) : 'null'}
		};`
				: null
		}

		${component.extract_javascript(component.ast.module)}

		${component.fully_hoisted}

		const ${name} = @create_ssr_component(($$result, $$props, $$bindings, #slots) => {
			${blocks}
		});
	`;
	return { js, css };
}

/** @param {import('../nodes/interfaces.js').INode[]} nodes */
function trim(nodes) {
	let start = 0;
	for (; start < nodes.length; start += 1) {
		const node = /** @type {import('../nodes/Text.js').default} */ (nodes[start]);
		if (node.type !== 'Text') break;
		node.data = node.data.replace(/^\s+/, '');
		if (node.data) break;
	}
	let end = nodes.length;
	for (; end > start; end -= 1) {
		const node = /** @type {import('../nodes/Text.js').default} */ (nodes[end - 1]);
		if (node.type !== 'Text') break;
		node.data = node.data.trimRight();
		if (node.data) break;
	}
	return nodes.slice(start, end);
}
