/**
 * Router Pattern Matching
 *
 * Route pattern compilation and matching utilities.
 */
import { debugLog, isRouterDebugEnabled } from "./logging.js";
/**
 * Parse a route pattern into segments
 *
 * Supports:
 * - Static: /blog, /about
 * - Params: /:slug, /:id
 * - Optional: /:locale?, /:page?
 * - Constrained: /:locale(en|gb), /:type(post|page)
 * - Optional + Constrained: /:locale(en|gb)?
 * - Wildcard: /*
 */
export function parsePattern(pattern) {
    const segments = [];
    // Match: /segment where segment can be:
    // - static text
    // - :param
    // - :param?
    // - :param(a|b)
    // - :param(a|b)?
    // - *
    const segmentRegex = /\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)?|(\*)|([^/]+))/g;
    let match;
    while ((match = segmentRegex.exec(pattern)) !== null) {
        const [, , paramName, , constraint, optional, wildcard, staticText] = match;
        if (wildcard) {
            segments.push({ type: "wildcard", value: "*", optional: false });
        }
        else if (paramName) {
            segments.push({
                type: "param",
                value: paramName,
                optional: optional === "?",
                constraint: constraint ? constraint.split("|") : undefined,
            });
        }
        else if (staticText) {
            segments.push({ type: "static", value: staticText, optional: false });
        }
    }
    return segments;
}
// Module-level cache for compiled patterns. Route patterns are a finite set
// defined at build time, so this map is bounded by the number of routes.
const compiledPatternCache = new Map();
/**
 * Get a compiled pattern from cache or compile and cache it.
 * Avoids O(routes) regex compilations per request in the fallback path.
 */
export function getCompiledPattern(pattern) {
    let compiled = compiledPatternCache.get(pattern);
    if (compiled)
        return compiled;
    compiled = compilePattern(pattern);
    compiledPatternCache.set(pattern, compiled);
    return compiled;
}
/**
 * Return the current size of the compiled pattern cache.
 * Exposed for testing.
 */
export function getPatternCacheSize() {
    return compiledPatternCache.size;
}
/**
 * Clear the compiled pattern cache.
 * Exposed for testing.
 */
export function clearPatternCache() {
    compiledPatternCache.clear();
}
/**
 * Compile a route pattern to regex
 *
 * Supports:
 * - Static segments: /blog, /about
 * - Dynamic params: /:slug, /:id
 * - Optional params: /:locale?, /:page?
 * - Constrained params: /:locale(en|gb)
 * - Optional + constrained: /:locale(en|gb)?
 * - Wildcard: /*
 *
 * @example
 * compilePattern("/blog/:slug")           // matches /blog/hello
 * compilePattern("/:locale?/blog")        // matches /blog or /en/blog
 * compilePattern("/:locale(en|gb)/blog")  // matches /en/blog or /gb/blog
 * compilePattern("/:locale(en|gb)?/blog") // matches /blog, /en/blog, or /gb/blog
 */
export function compilePattern(pattern) {
    // Detect if pattern has trailing slash (but not just "/")
    const hasTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
    // Remove trailing slash for parsing (we'll add it back to regex if needed)
    const normalizedPattern = hasTrailingSlash ? pattern.slice(0, -1) : pattern;
    const segments = parsePattern(normalizedPattern);
    const paramNames = [];
    const optionalParams = new Set();
    let regexPattern = "";
    for (const segment of segments) {
        if (segment.type === "wildcard") {
            paramNames.push("*");
            regexPattern += "/(.*)";
        }
        else if (segment.type === "param") {
            paramNames.push(segment.value);
            const valuePattern = segment.constraint
                ? `(${segment.constraint.join("|")})`
                : "([^/]+)";
            if (segment.optional) {
                optionalParams.add(segment.value);
                // Optional: make the whole /segment optional
                regexPattern += `(?:/${valuePattern})?`;
            }
            else {
                regexPattern += `/${valuePattern}`;
            }
        }
        else {
            // Static segment
            regexPattern += `/${escapeRegex(segment.value)}`;
        }
    }
    // Handle root path
    if (regexPattern === "") {
        regexPattern = "/";
    }
    // Add trailing slash to regex if pattern has one
    if (hasTrailingSlash) {
        regexPattern += "/";
    }
    return {
        regex: new RegExp(`^${regexPattern}$`),
        paramNames,
        optionalParams,
        hasTrailingSlash,
    };
}
/**
 * Escape special regex characters in a string
 */
function escapeRegex(str) {
    return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
/**
 * Extract the static prefix from a route pattern.
 * Returns everything before the first param/wildcard.
 *
 * Called ONCE at registration time, not at match time.
 *
 * Examples:
 * - "/api" → "/api"
 * - "/site/:locale" → "/site"
 * - "/:locale" → ""
 * - "/admin/users/:id" → "/admin/users"
 * - "/api/*" → "/api"
 */
export function extractStaticPrefix(pattern) {
    if (!pattern || pattern === "/")
        return "";
    // Find the first occurrence of : or *
    const paramIndex = pattern.indexOf(":");
    const wildcardIndex = pattern.indexOf("*");
    let cutIndex = -1;
    if (paramIndex !== -1 && wildcardIndex !== -1) {
        cutIndex = Math.min(paramIndex, wildcardIndex);
    }
    else if (paramIndex !== -1) {
        cutIndex = paramIndex;
    }
    else if (wildcardIndex !== -1) {
        cutIndex = wildcardIndex;
    }
    if (cutIndex === -1) {
        // No params or wildcards - entire pattern is static
        return pattern;
    }
    if (cutIndex === 0) {
        // Pattern starts with : or * - no static prefix
        return "";
    }
    // Find the last / before the param
    const lastSlash = pattern.lastIndexOf("/", cutIndex - 1);
    if (lastSlash === -1 || lastSlash === 0) {
        return "";
    }
    return pattern.slice(0, lastSlash);
}
/**
 * Type guard to check if result is a lazy evaluation needed response
 */
export function isLazyEvaluationNeeded(result) {
    return result !== null && "lazyEntry" in result;
}
// Debug stats for route matching (only in debug mode)
let debugEnabled = false;
let debugStats = { entriesChecked: 0, entriesSkipped: 0, routesChecked: 0 };
export function enableMatchDebug(enabled) {
    debugEnabled = enabled;
}
export function getMatchDebugStats() {
    return { entriesChecked: debugStats.entriesChecked, entriesSkipped: debugStats.entriesSkipped, routesChecked: debugStats.routesChecked };
}
export function findMatch(pathname, routesEntries) {
    const effectiveDebug = debugEnabled || isRouterDebugEnabled();
    if (effectiveDebug) {
        debugStats = { entriesChecked: 0, entriesSkipped: 0, routesChecked: 0 };
        debugLog("findMatch", "start", { pathname, entries: routesEntries.length });
        for (const e of routesEntries) {
            debugLog("findMatch", "entry", {
                prefix: e.prefix,
                staticPrefix: e.staticPrefix,
                routeCount: Object.keys(e.routes).length,
            });
        }
    }
    const pathnameHasTrailingSlash = pathname.length > 1 && pathname.endsWith("/");
    // Try alternate pathname for redirect matching
    const alternatePathname = pathnameHasTrailingSlash
        ? pathname.slice(0, -1)
        : pathname + "/";
    for (const entry of routesEntries) {
        // Short-circuit: skip entry if pathname doesn't start with static prefix
        // staticPrefix is pre-computed at registration time, so this is O(1)
        if (entry.staticPrefix && !pathname.startsWith(entry.staticPrefix)) {
            if (effectiveDebug) {
                debugStats.entriesSkipped++;
                debugLog("findMatch", "skipped entry", {
                    prefix: entry.prefix,
                    staticPrefix: entry.staticPrefix,
                });
            }
            continue;
        }
        // Check if this is a lazy entry that needs evaluation
        // When staticPrefix matches but routes are not yet populated, signal caller to evaluate
        if (entry.lazy && !entry.lazyEvaluated) {
            if (effectiveDebug) {
                debugLog("findMatch", "lazy entry requires evaluation", {
                    staticPrefix: entry.staticPrefix,
                });
            }
            return { lazyEntry: entry };
        }
        if (effectiveDebug) {
            debugStats.entriesChecked++;
        }
        const routeEntries = Object.entries(entry.routes);
        for (const [routeKey, pattern] of routeEntries) {
            if (effectiveDebug) {
                debugStats.routesChecked++;
            }
            // Join prefix and pattern, handling edge cases
            let fullPattern;
            if (entry.prefix === "" || entry.prefix === "/") {
                fullPattern = pattern;
            }
            else if (pattern === "/" || pattern === "") {
                fullPattern = entry.prefix;
            }
            else {
                fullPattern = entry.prefix + pattern;
            }
            const { regex, paramNames, optionalParams, hasTrailingSlash } = getCompiledPattern(fullPattern);
            // Get trailing slash mode for this route (per-route config or pattern-based)
            const trailingSlashMode = entry.trailingSlash?.[routeKey];
            // Prerender flag from entry metadata (set by urls() for prerender handlers)
            const prFlag = entry.prerenderRouteKeys?.has(routeKey) ? { pr: true } : {};
            // Try exact match first
            const match = regex.exec(pathname);
            if (match) {
                const params = {};
                paramNames.forEach((name, index) => {
                    params[name] = match[index + 1] ?? "";
                });
                if (effectiveDebug) {
                    debugLog("findMatch", "matched route", {
                        routeKey,
                        pattern: fullPattern,
                        stats: { ...debugStats },
                    });
                }
                // Check if trailing slash mode requires redirect even on exact match
                if (trailingSlashMode === "always" && !pathnameHasTrailingSlash && pathname !== "/") {
                    // Mode says always have trailing slash, but pathname doesn't have it
                    return { entry, routeKey, params, optionalParams, redirectTo: pathname + "/", ...prFlag };
                }
                else if (trailingSlashMode === "never" && pathnameHasTrailingSlash) {
                    // Mode says never have trailing slash, but pathname has it
                    return { entry, routeKey, params, optionalParams, redirectTo: pathname.slice(0, -1), ...prFlag };
                }
                return { entry, routeKey, params, optionalParams, ...prFlag };
            }
            // Try alternate pathname (opposite trailing slash)
            const altMatch = regex.exec(alternatePathname);
            if (altMatch) {
                const params = {};
                paramNames.forEach((name, index) => {
                    params[name] = altMatch[index + 1] ?? "";
                });
                // Determine redirect behavior based on mode
                if (trailingSlashMode === "ignore") {
                    // Match without redirect
                    return { entry, routeKey, params, optionalParams, ...prFlag };
                }
                else if (trailingSlashMode === "never") {
                    // Redirect to no trailing slash
                    if (pathnameHasTrailingSlash) {
                        return { entry, routeKey, params, optionalParams, redirectTo: alternatePathname, ...prFlag };
                    }
                    return { entry, routeKey, params, optionalParams, ...prFlag };
                }
                else if (trailingSlashMode === "always") {
                    // Redirect to with trailing slash
                    if (!pathnameHasTrailingSlash) {
                        return { entry, routeKey, params, optionalParams, redirectTo: alternatePathname, ...prFlag };
                    }
                    return { entry, routeKey, params, optionalParams, ...prFlag };
                }
                else {
                    // No explicit mode - use pattern-based detection
                    // Redirect to canonical form (what the pattern defines)
                    const canonicalPath = hasTrailingSlash ? alternatePathname : pathname.slice(0, -1);
                    return { entry, routeKey, params, optionalParams, redirectTo: canonicalPath, ...prFlag };
                }
            }
        }
    }
    return null;
}
/**
 * Traverse from entry to bottom to top, yielding each EntryData
 * e.g. {child -> parent -> grandparent ...}
 */
export function* traverseBack(entry) {
    let current = entry;
    const items = [];
    while (current !== null) {
        items.push(current); // Move up to next parent
        current = current.parent;
    }
    for (let i = items.length - 1; i >= 0; i--) {
        yield items[i];
    }
}
//# sourceMappingURL=pattern-matching.js.map