/**
* Compiles the router instance into a faster route-matching function.
*
* **IMPORTANT:** `compileRouter` requires eval support with `new Function()` in the runtime for JIT compilation.
*
* @example
* import { createRouter, addRoute } from "rou3";
* import { compileRouter } from "rou3/compiler";
* const router = createRouter();
* // [add some routes]
* const findRoute = compileRouter(router);
* const matchAll = compileRouter(router, { matchAll: true });
* findRoute("GET", "/path/foo/bar");
*
* @param router - The router context to compile.
*/
function compileRouter(router, opts) {
	const ctx = {
		opts: opts || {},
		router,
		data: []
	};
	const compiled = compileRouteMatch(ctx);
	return new Function(...ctx.data.map((_, i) => `$${i}`), `return(m,p)=>{${compiled}}`)(...ctx.data);
}
/**
* Compile the router instance into a compact runnable code.
*
* **IMPORTANT:** Route data must be serializable to JSON (i.e., no functions or classes) or implement the `toJSON()` method to render custom code or you can pass custom `serialize` function in options.
*
* @example
* import { createRouter, addRoute } from "rou3";
* import { compileRouterToString } from "rou3/compiler";
* const router = createRouter();
* // [add some routes with serializable data]
* const compilerCode = compileRouterToString(router, "findRoute");
* // "const findRoute=(m, p) => {}"
*/
function compileRouterToString(router, functionName, opts) {
	const ctx = {
		opts: opts || {},
		router,
		data: [],
		compileToString: true
	};
	let compiled = `(m,p)=>{${compileRouteMatch(ctx)}}`;
	if (ctx.data.length > 0) compiled = `/* @__PURE__ */ (() => { ${`const ${ctx.data.map((v, i) => `$${i}=${v}`).join(",")};`}; return ${compiled}})()`;
	return functionName ? `const ${functionName}=${compiled};` : compiled;
}
function compileRouteMatch(ctx) {
	let code = "";
	const staticNodes = /* @__PURE__ */ new Set();
	for (const key in ctx.router.static) {
		const node = ctx.router.static[key];
		if (node?.methods) {
			staticNodes.add(node);
			code += `if(p===${JSON.stringify(key.replace(/\/$/, "") || "/")}){${compileMethodMatch(ctx, node.methods, [], -1)}}`;
		}
	}
	const match = compileNode(ctx, ctx.router.root, [], 0, staticNodes);
	if (match) code += `let s=p.split("/"),l=s.length-1;${match}`;
	if (!code) return ctx.opts?.matchAll ? `return [];` : "";
	return `${ctx.opts?.matchAll ? `let r=[];` : ""}if(p.charCodeAt(p.length-1)===47)p=p.slice(0,-1)||"/";${code}${ctx.opts?.matchAll ? "return r;" : ""}`;
}
function compileMethodMatch(ctx, methods, params, currentIdx) {
	let code = "";
	for (const key in methods) {
		const matchers = methods[key];
		if (matchers && matchers?.length > 0) {
			if (key !== "") code += `if(m==="${key}")${matchers.length > 1 ? "{" : ""}`;
			const _matchers = matchers.map((m) => compileFinalMatch(ctx, m, currentIdx, params)).sort((a, b) => b.weight - a.weight);
			for (const matcher of _matchers) code += matcher.code;
			if (key !== "") code += matchers.length > 1 ? "}" : "";
		}
	}
	return code;
}
function compileFinalMatch(ctx, data, currentIdx, params) {
	let ret = `{data:${serializeData(ctx, data.data)}`;
	const conditions = [];
	const { paramsMap, paramsRegexp } = data;
	if (paramsMap && paramsMap.length > 0) {
		if (!paramsMap[paramsMap.length - 1][2] && currentIdx !== -1) conditions.push(`l>=${currentIdx}`);
		for (let i = 0; i < paramsRegexp.length; i++) {
			const regexp = paramsRegexp[i];
			if (!regexp) continue;
			conditions.push(`${regexp.toString()}.test(s[${i + 1}])`);
		}
		ret += ",params:{";
		for (let i = 0; i < paramsMap.length; i++) {
			const map = paramsMap[i];
			ret += typeof map[1] === "string" ? `${JSON.stringify(map[1])}:${params[i]},` : `...(${map[1].toString()}.exec(${params[i]}))?.groups,`;
		}
		ret += "}";
	}
	return {
		code: (conditions.length > 0 ? `if(${conditions.join("&&")})` : "") + (ctx.opts?.matchAll ? `r.unshift(${ret}});` : `return ${ret}};`),
		weight: conditions.length
	};
}
function compileNode(ctx, node, params, startIdx, staticNodes) {
	let code = "";
	if (node.methods && !staticNodes.has(node)) {
		const match = compileMethodMatch(ctx, node.methods, params, node.key === "*" ? startIdx : -1);
		if (match) {
			const hasLastOptionalParam = node.key === "*";
			code += `if(l===${startIdx}${hasLastOptionalParam ? `||l===${startIdx - 1}` : ""}){${match}}`;
		}
	}
	if (node.static) for (const key in node.static) {
		const match = compileNode(ctx, node.static[key], params, startIdx + 1, staticNodes);
		if (match) code += `if(s[${startIdx + 1}]===${JSON.stringify(key)}){${match}}`;
	}
	if (node.param) {
		const match = compileNode(ctx, node.param, [...params, `s[${startIdx + 1}]`], startIdx + 1, staticNodes);
		if (match) code += match;
	}
	if (node.wildcard) {
		const { wildcard } = node;
		if (wildcard.static || wildcard.param || wildcard.wildcard) throw new Error("Compiler mode does not support patterns after wildcard");
		if (wildcard.methods) {
			const match = compileMethodMatch(ctx, wildcard.methods, [...params, `s.slice(${startIdx + 1}).join('/')`], startIdx);
			if (match) code += match;
		}
	}
	return code;
}
function serializeData(ctx, value) {
	if (ctx.compileToString) if (ctx.opts?.serialize) value = ctx.opts.serialize(value);
	else if (typeof value?.toJSON === "function") value = value.toJSON();
	else value = JSON.stringify(value);
	let index = ctx.data.indexOf(value);
	if (index === -1) {
		ctx.data.push(value);
		index = ctx.data.length - 1;
	}
	return `$${index}`;
}

export { compileRouter, compileRouterToString };