"use strict";
/**
 * HotStaq v0.9.0 client-side runtime.
 *
 * Target: the bundled app.js for `hotstaq build --static` output. Loaded
 * once from index.html, it registers the routes that the build compiler
 * generated into the same bundle, then mounts the correct <template>
 * fragment into #app on load and on every navigation.
 *
 * Covers HS090-10 (Hot.navigate), HS090-11 (template mount + preamble
 * re-execution), and a basic HS090-13 (link interception). HS090-12
 * morph swap, HS090-14 scroll restoration, HS090-16/17 preload tiers
 * and prefetch-on-hover arrive in follow-up commits.
 */
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerRoute = registerRoute;
exports.registerApi = registerApi;
exports.registerChunk = registerChunk;
exports.prefetchRoute = prefetchRoute;
exports.configureMount = configureMount;
exports.navigate = navigate;
exports.mount = mount;
exports.getLastContext = getLastContext;
exports.installLinkInterceptor = installLinkInterceptor;
exports.start = start;
const state = {
    mountSelector: "#app",
    routes: new Map(),
    api: {},
    chunkUrls: new Map(),
    chunkLoads: new Map()
};
/**
 * Register a compiled route with the runtime. Called from the bundler-
 * generated entry file; apps shouldn't need to call this directly.
 */
function registerRoute(entry) {
    state.routes.set(entry.path, entry);
}
/**
 * Register the auto-generated Web API client (HS090-8). The builder's
 * entry file calls this before the first mount.
 */
function registerApi(api) {
    state.api = api;
}
/**
 * HS090-16: register a lazy/never route's chunk URL. When `mount(path)`
 * is called for an unregistered route whose chunk is known, the runtime
 * fetches the chunk (which then calls registerRoute) and retries mount.
 */
function registerChunk(path, url) {
    state.chunkUrls.set(path, url);
}
/**
 * Explicitly load a route's chunk without mounting. Used by hover
 * prefetch (HS090-17) and callable from preambles that know the user
 * is about to navigate.
 */
function prefetchRoute(path) {
    return (loadRouteChunk(path));
}
function loadRouteChunk(path) {
    if (state.routes.has(path))
        return (Promise.resolve());
    const inflight = state.chunkLoads.get(path);
    if (inflight)
        return (inflight);
    const url = state.chunkUrls.get(path);
    if (!url)
        return (Promise.resolve());
    const p = new Promise((resolve, reject) => {
        const s = document.createElement("script");
        s.src = url;
        s.async = true;
        s.onload = () => resolve();
        s.onerror = () => reject(new Error(`[hs090] failed to load chunk ${url}`));
        document.head.appendChild(s);
    });
    state.chunkLoads.set(path, p);
    return (p);
}
/**
 * Change the CSS selector the runtime mounts into. Defaults to "#app".
 */
function configureMount(selector) {
    state.mountSelector = selector;
}
/**
 * Navigate to a new path without a full page reload.
 */
function navigate(path_1) {
    return __awaiter(this, arguments, void 0, function* (path, opts = {}) {
        if (opts.replace)
            history.replaceState(null, "", path);
        else
            history.pushState(null, "", path);
        yield mount();
        // HS090-14: scroll to top for fresh navigations, restore on popstate.
        window.scrollTo(0, 0);
    });
}
/**
 * Look up and mount the route that matches `location.pathname` (or
 * the `targetPath` override if given). Re-entrant: a second call aborts
 * the in-flight preamble of the first via an AbortController signal on
 * the HotCtx.
 */
function mount(targetPath) {
    return __awaiter(this, void 0, void 0, function* () {
        const path = targetPath || location.pathname;
        let route = resolveRoute(path);
        if (!route) {
            // HS090-16: maybe it's a lazy route whose chunk hasn't loaded yet.
            const chunkPath = matchChunkPath(path);
            if (chunkPath) {
                try {
                    yield loadRouteChunk(chunkPath);
                }
                catch (err) {
                    console.error(err);
                    return;
                }
                route = resolveRoute(path);
            }
        }
        if (!route) {
            console.error(`[hs090] no route registered for ${JSON.stringify(path)}. ` +
                `Known: ${Array.from(state.routes.keys()).join(", ")}`);
            return;
        }
        const host = document.querySelector(state.mountSelector);
        if (!host) {
            console.error(`[hs090] mount host ${state.mountSelector} not found in DOM.`);
            return;
        }
        const template = document.getElementById(route.templateId);
        if (!template) {
            console.error(`[hs090] <template id="${route.templateId}"> not found for route ${path}.`);
            return;
        }
        // Cancel any preamble still running for the previous route.
        if (state.currentAbort)
            state.currentAbort.abort();
        const abort = new AbortController();
        state.currentAbort = abort;
        host.innerHTML = "";
        // Pure-static templates have their HTML inline in the stash; we
        // clone that first so the initial paint is free of preamble latency.
        // Dynamic routes (.hott files that contain <* *> preambles) have an
        // empty stash template and rely on the compiled preamble to echo all
        // content in source order — that preserves admin-panel's header →
        // body → footer layering.
        const stashHtml = template.innerHTML || "";
        const pureStatic = stashHtml.trim() !== "";
        if (pureStatic)
            host.appendChild(document.importNode(template.content, true));
        const buffer = { html: "" };
        const ctx = buildHotCtx(host, abort, buffer);
        try {
            if (route.preamble)
                yield route.preamble(ctx);
        }
        catch (err) {
            if (abort.signal.aborted)
                return;
            console.error(`[hs090] preamble for ${path} threw:`, err);
        }
        if (abort.signal.aborted)
            return;
        // Single innerHTML parse of the accumulated echoes — lets unclosed
        // tags in one echo get closed by a later echo (admin-panel's
        // header → body → footer layering needs this).
        if (!pureStatic && buffer.html)
            host.innerHTML = buffer.html;
        // Re-execute inline scripts: the tokenizer left empty placeholder
        // <script data-hott-script="hott-sN"> nodes; replace them with real
        // scripts so the browser runs the bodies. Works whether they
        // landed via template clone or preamble echo.
        executeInlineScripts(host, route.scripts || []);
    });
}
/**
 * Return the HotCtx singleton for the most recent mount. Intended for
 * debugging from the console — preambles receive their own hotCtx
 * parameter and should not reach for this.
 */
function getLastContext() {
    return (lastCtx);
}
let lastCtx = null;
/**
 * HS090-14 scroll restoration: stash scroll position on every nav and
 * restore on popstate.
 */
function saveScrollPosition() {
    try {
        const existing = history.state || {};
        history.replaceState(Object.assign({}, existing, { __hs090_scroll: { x: window.scrollX, y: window.scrollY } }), "", location.href);
    }
    catch ( /* replaceState can fail in some sandboxes; non-critical */_a) { /* replaceState can fail in some sandboxes; non-critical */ }
}
function restoreScrollPosition() {
    const s = history.state || {};
    if (s.__hs090_scroll)
        window.scrollTo(s.__hs090_scroll.x || 0, s.__hs090_scroll.y || 0);
    else
        window.scrollTo(0, 0);
}
/**
 * Intercept link clicks (HS090-13) + form submits (HS090-12 subset) —
 * any same-origin <a href> or <form method=get action=...> routes via
 * navigate() instead of a full page load. Links with [download],
 * [target=_blank], or [data-spa-ignore] skip; forms with
 * [data-spa-ignore] skip.
 */
function installLinkInterceptor() {
    document.addEventListener("click", (event) => {
        if (event.defaultPrevented)
            return;
        if (event.button !== 0)
            return;
        if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
            return;
        const a = findAnchor(event.target);
        if (!a)
            return;
        const url = intraAppHref(a);
        if (!url)
            return;
        event.preventDefault();
        saveScrollPosition();
        navigate(url.pathname + url.search + url.hash);
    });
    // HS090-12 form interception: GET forms that target same-origin get
    // serialized into a querystring and navigated via pushState. POST
    // forms stay as full submits — preambles handle those via hotCtx.api.
    document.addEventListener("submit", (event) => {
        const form = event.target;
        if (!form || form.tagName !== "FORM")
            return;
        if (form.hasAttribute("data-spa-ignore"))
            return;
        if ((form.method || "get").toLowerCase() !== "get")
            return;
        const action = form.getAttribute("action") || location.pathname;
        let actionUrl;
        try {
            actionUrl = new URL(action, location.href);
        }
        catch (_a) {
            return;
        }
        if (actionUrl.origin !== location.origin)
            return;
        event.preventDefault();
        const data = new FormData(form);
        const qs = new URLSearchParams();
        data.forEach((v, k) => { if (typeof v === "string")
            qs.append(k, v); });
        const q = qs.toString();
        const dest = actionUrl.pathname + (q ? "?" + q : "") + actionUrl.hash;
        saveScrollPosition();
        navigate(dest);
    });
    // HS090-17: hover/focus prefetch. Only warms the chunk (no mount).
    // Uses `mouseover` (buubbles) with a WeakSet to debounce per anchor.
    const prefetched = new WeakSet();
    const maybePrefetch = (a) => {
        if (!a || prefetched.has(a))
            return;
        const url = intraAppHref(a);
        if (!url)
            return;
        const chunkPath = matchChunkPath(url.pathname);
        if (!chunkPath)
            return;
        const route = state.routes.get(chunkPath);
        if (route && route.preload === "never")
            return;
        prefetched.add(a);
        loadRouteChunk(chunkPath).catch(() => { });
    };
    document.addEventListener("mouseover", (e) => {
        maybePrefetch(findAnchor(e.target));
    });
    document.addEventListener("focusin", (e) => {
        maybePrefetch(findAnchor(e.target));
    });
    window.addEventListener("popstate", () => __awaiter(this, void 0, void 0, function* () {
        yield mount();
        restoreScrollPosition();
    }));
}
function findAnchor(start) {
    let el = start;
    while (el && el.tagName !== "A")
        el = el.parentElement;
    return el;
}
function intraAppHref(a) {
    if (!a)
        return (null);
    if (a.hasAttribute("data-spa-ignore"))
        return (null);
    if (a.hasAttribute("download"))
        return (null);
    if (a.target && a.target !== "" && a.target !== "_self")
        return (null);
    const href = a.getAttribute("href") || "";
    if (!href || href.startsWith("#") || href.startsWith("mailto:") ||
        href.startsWith("tel:") || /^[a-z]+:\/\//i.test(href)) {
        return (null);
    }
    const url = new URL(a.href);
    if (url.origin !== location.origin)
        return (null);
    return (url);
}
function matchChunkPath(path) {
    if (state.chunkUrls.has(path))
        return (path);
    const trimmed = path.replace(/\/+$/, "") || "/";
    if (state.chunkUrls.has(trimmed))
        return (trimmed);
    const withSlash = path.endsWith("/") ? path : path + "/";
    if (state.chunkUrls.has(withSlash))
        return (withSlash);
    return ("");
}
/**
 * Entry point the bundler-generated entry file invokes.
 */
function start() {
    // Legacy HotStaq.min.js (when shipped via jsFiles) registers
    // hotStaqWebStart on window.load; that callback copies HotStaqWeb
    // keys onto window (including window.Hot) — which clobbers any
    // Hot.CurrentPage.processor we set up earlier. We defer our initial
    // mount until after window.load so the legacy startup runs first
    // and our bootstrap gets the last word on Hot.CurrentPage.
    const runLater = () => {
        // Give hotStaqWebStart's microtask chain one macrotask to settle
        // (it does its work inside a setTimeout(…, 50)).
        setTimeout(() => initialMount(), 70);
    };
    if (document.readyState === "complete")
        runLater();
    else
        window.addEventListener("load", runLater, { once: true });
}
function initialMount() {
    installLinkInterceptor();
    mount();
}
// ── Internals ──────────────────────────────────────────────────────────
function resolveRoute(path) {
    if (state.routes.has(path))
        return (state.routes.get(path));
    // Tolerate trailing slash differences.
    const trimmed = path.replace(/\/+$/, "") || "/";
    if (state.routes.has(trimmed))
        return (state.routes.get(trimmed));
    const withSlash = path.endsWith("/") ? path : path + "/";
    if (state.routes.has(withSlash))
        return (state.routes.get(withSlash));
    return (undefined);
}
function buildHotCtx(host, abort, buffer) {
    const ctx = {
        cookies: {
            get: cookieGet,
            set: cookieSet,
            remove: cookieRemove
        },
        search: new URLSearchParams(location.search),
        pathname: location.pathname,
        params: {}, // HS090 future: pattern-matched path params.
        api: state.api,
        getJSON(url) {
            return __awaiter(this, void 0, void 0, function* () {
                const res = yield fetch(url, { signal: abort.signal });
                if (!res.ok)
                    throw new Error(`getJSON ${url} → HTTP ${res.status}`);
                return (res.json());
            });
        },
        import(pkg) {
            return __awaiter(this, void 0, void 0, function* () {
                // 1) If a legacy-compat bootstrap has populated the
                //    processor's module map (via the <script src> load
                //    of hotstaq_modules/{name}/index.js), just return
                //    the cached HotModule.
                const w = (typeof window !== "undefined" ? window : null);
                if (w && w.Hot && w.Hot.CurrentPage && w.Hot.CurrentPage.processor) {
                    const proc = w.Hot.CurrentPage.processor;
                    if (typeof proc.getModule === "function") {
                        const cached = proc.getModule(pkg);
                        if (cached)
                            return (cached);
                    }
                }
                // 2) Fetch + eval hotstaq_modules/{pkg}/index.js the way
                //    legacy Hot.import does. The file expects a
                //    HotStaqWeb.HotModule global already on the page.
                try {
                    const res = yield fetch(`./hotstaq_modules/${pkg}/index.js`);
                    if (res.ok && w) {
                        const body = yield res.text();
                        // eslint-disable-next-line no-new-func
                        const fn = new Function(body);
                        const mod = fn.call(w);
                        if (w.Hot && w.Hot.CurrentPage && w.Hot.CurrentPage.processor &&
                            typeof w.Hot.CurrentPage.processor.addModule === "function") {
                            w.Hot.CurrentPage.processor.addModule(pkg, mod);
                        }
                        return (mod);
                    }
                }
                catch ( /* fall through */_a) { /* fall through */ }
                return ({});
            });
        },
        includeStash(id) {
            const el = document.getElementById(`hott-partial--${id}`);
            if (!el)
                return ("");
            // Partial stash entries ship as <script type="text/html">
            // whose .textContent preserves the raw HTML (including
            // intentionally unbalanced tag pairs between header/footer
            // partials).
            return (el.textContent || "");
        },
        echo(html) {
            // Accumulate echoes into a string buffer so the final
            // innerHTML parse sees a single well-formed fragment.
            // Legacy .hott partials (admin-panel's header/footer) rely
            // on unclosed tags from one include being CLOSED by a later
            // sibling include — per-call insertAdjacentHTML parses each
            // piece independently, which auto-closes unbalanced tags
            // and breaks that model.
            if (html == null)
                return;
            buffer.html += String(html);
        },
        jsonRequest(url, body, auth) {
            return __awaiter(this, void 0, void 0, function* () {
                const headers = {
                    "Accept": "application/json",
                    "Content-Type": "application/json"
                };
                if (auth)
                    headers["Authorization"] = "Bearer " + auth;
                const res = yield fetch(url, {
                    method: "POST",
                    headers,
                    body: JSON.stringify(body),
                    signal: abort.signal
                });
                const text = yield res.text();
                try {
                    return (JSON.parse(text));
                }
                catch (_a) {
                    return (text);
                }
            });
        },
        includeJS(url_1) {
            return __awaiter(this, arguments, void 0, function* (url, args = []) {
                // Legacy-compat helper: Hot.includeJS("./js/foo.js", [arg1, arg2])
                // fetches the script body, evaluates it as a function body whose
                // `this` is the hotCtx, and returns whatever it yields.
                const abs = new URL(url, location.href);
                const res = yield fetch(abs.toString(), { signal: abort.signal });
                if (!res.ok)
                    throw new Error(`includeJS ${url} → HTTP ${res.status}`);
                const body = yield res.text();
                const argNames = args.map((_, i) => `_a${i}`);
                // Wrap in an async function so `await` inside works.
                // eslint-disable-next-line no-new-func
                const fn = new Function(...argNames, `return (async () => { ${body}\nreturn (typeof __hott_export !== 'undefined' ? __hott_export : undefined); })();`);
                return (fn.apply(ctx, args));
            });
        }
    };
    lastCtx = ctx;
    return (ctx);
}
function executeInlineScripts(host, bodies) {
    const placeholders = host.querySelectorAll("script[data-hott-script]");
    for (const placeholder of Array.from(placeholders)) {
        const id = placeholder.getAttribute("data-hott-script");
        const match = id ? id.match(/^hott-s(\d+)$/) : null;
        const idx = match ? parseInt(match[1], 10) : -1;
        const body = (idx >= 0 && idx < bodies.length) ? bodies[idx] : "";
        const real = document.createElement("script");
        for (const attr of Array.from(placeholder.attributes)) {
            if (attr.name === "data-hott-script")
                continue;
            real.setAttribute(attr.name, attr.value);
        }
        real.textContent = body;
        placeholder.replaceWith(real);
    }
}
function cookieGet(name) {
    const needle = encodeURIComponent(name) + "=";
    for (const pair of document.cookie.split("; ")) {
        if (pair.startsWith(needle))
            return (decodeURIComponent(pair.substring(needle.length)));
    }
    return (null);
}
function cookieSet(name, value, opts = {}) {
    let c = encodeURIComponent(name) + "=" + encodeURIComponent(value);
    if (opts.path)
        c += "; path=" + opts.path;
    if (opts.maxAge != null)
        c += "; max-age=" + opts.maxAge;
    if (opts.expires)
        c += "; expires=" + opts.expires;
    if (opts.domain)
        c += "; domain=" + opts.domain;
    if (opts.secure)
        c += "; secure";
    if (opts.sameSite)
        c += "; samesite=" + opts.sameSite;
    document.cookie = c;
}
function cookieRemove(name, opts = {}) {
    cookieSet(name, "", Object.assign({}, opts, { maxAge: 0 }));
}
//# sourceMappingURL=HotStaticRuntime.js.map