import { visit } from 'estree-util-visit';
import { toHtml } from 'hast-util-to-html';

// accessing untyped hast and mdx types
type Node = any;

export interface OptimizeOptions {
	customComponentNames?: string[];
}

const exportConstComponentsRe = /export\s+const\s+components\s*=/;

/**
 * For MDX only, collapse static subtrees of the hast into `set:html`. Subtrees
 * do not include any MDX elements.
 *
 * This optimization reduces the JS output as more content are represented as a
 * string instead, which also reduces the AST size that Rollup holds in memory.
 */
export function rehypeOptimizeStatic(options?: OptimizeOptions) {
	return (tree: any) => {
		// A set of non-static components to avoid collapsing when walking the tree
		// as they need to be preserved as JSX to be rendered dynamically.
		const customComponentNames = new Set<string>(options?.customComponentNames);

		// Find `export const components = { ... }` and get it's object's keys to be
		// populated into `customComponentNames`. This configuration is used to render
		// some HTML elements as custom components, and we also want to avoid collapsing them.
		for (const child of tree.children) {
			if (child.type === 'mdxjsEsm' && exportConstComponentsRe.test(child.value)) {
				// Try to loosely get the object property nodes
				const objectPropertyNodes = child.data.estree.body[0]?.declarations?.[0]?.init?.properties;
				if (objectPropertyNodes) {
					for (const objectPropertyNode of objectPropertyNodes) {
						const componentName = objectPropertyNode.key?.name ?? objectPropertyNode.key?.value;
						if (componentName) {
							customComponentNames.add(componentName);
						}
					}
				}
			}
		}

		// All possible elements that could be the root of a subtree
		const allPossibleElements = new Set<Node>();
		// The current collapsible element stack while traversing the tree
		const elementStack: Node[] = [];

		visit(tree, {
			enter(node) {
				// @ts-expect-error read tagName naively
				const isCustomComponent = node.tagName && customComponentNames.has(node.tagName);
				// For nodes that can't be optimized, eliminate all elements in the
				// `elementStack` from the `allPossibleElements` set.
				if (node.type.startsWith('mdx') || isCustomComponent) {
					for (const el of elementStack) {
						allPossibleElements.delete(el);
					}
					// Micro-optimization: While this destroys the meaning of an element
					// stack for this node, things will still work but we won't repeatedly
					// run the above for other nodes anymore. If this is confusing, you can
					// comment out the code below when reading.
					elementStack.length = 0;
				}
				// For possible subtree root nodes, record them in `elementStack` and
				// `allPossibleElements` to be used in the "leave" hook below.
				// @ts-expect-error MDX types for `.type` is not enhanced because MDX isn't used directly
				if (node.type === 'element' || node.type === 'mdxJsxFlowElement') {
					elementStack.push(node);
					allPossibleElements.add(node);
				}
			},
			leave(node, _, __, parents) {
				// Do the reverse of the if condition above, popping the `elementStack`,
				// and consolidating `allPossibleElements` as a subtree root.
				// @ts-expect-error MDX types for `.type` is not enhanced because MDX isn't used directly
				if (node.type === 'element' || node.type === 'mdxJsxFlowElement') {
					elementStack.pop();
					// Many possible elements could be part of a subtree, in order to find
					// the root, we check the parent of the element we're popping. If the
					// parent exists in `allPossibleElements`, then we're definitely not
					// the root, so remove ourselves. This will work retroactively as we
					// climb back up the tree.
					const parent = parents[parents.length - 1];
					if (allPossibleElements.has(parent)) {
						allPossibleElements.delete(node);
					}
				}
			},
		});

		// For all possible subtree roots, collapse them into `set:html` and
		// strip of their children
		for (const el of allPossibleElements) {
			if (el.type === 'mdxJsxFlowElement') {
				el.attributes.push({
					type: 'mdxJsxAttribute',
					name: 'set:html',
					value: toHtml(el.children),
				});
			} else {
				el.properties['set:html'] = toHtml(el.children);
			}
			el.children = [];
		}
	};
}
