import { createSignal, getListener, getOwner, onCleanup, sharedConfig, startTransition } from "solid-js";
import { getRequestEvent, isServer } from "solid-js/web";
import { useNavigate, getIntent, getInPreloadFn } from "../routing.js";
const LocationHeader = "Location";
const PRELOAD_TIMEOUT = 5000;
const CACHE_TIMEOUT = 180000;
let cacheMap = new Map();
// cleanup forward/back cache
if (!isServer) {
    setInterval(() => {
        const now = Date.now();
        for (let [k, v] of cacheMap.entries()) {
            if (!v[4].count && now - v[0] > CACHE_TIMEOUT) {
                cacheMap.delete(k);
            }
        }
    }, 300000);
}
function getCache() {
    if (!isServer)
        return cacheMap;
    const req = getRequestEvent();
    if (!req)
        throw new Error("Cannot find cache context");
    return (req.router || (req.router = {})).cache || (req.router.cache = new Map());
}
/**
 * Revalidates the given cache entry/entries.
 */
export function revalidate(key, force = true) {
    return startTransition(() => {
        const now = Date.now();
        cacheKeyOp(key, entry => {
            force && (entry[0] = 0); //force cache miss
            entry[4][1](now); // retrigger live signals
        });
    });
}
export function cacheKeyOp(key, fn) {
    key && !Array.isArray(key) && (key = [key]);
    for (let k of cacheMap.keys()) {
        if (key === undefined || matchKey(k, key))
            fn(cacheMap.get(k));
    }
}
export function query(fn, name) {
    // prioritize GET for server functions
    if (fn.GET)
        fn = fn.GET;
    const cachedFn = ((...args) => {
        const cache = getCache();
        const intent = getIntent();
        const inPreloadFn = getInPreloadFn();
        const owner = getOwner();
        const navigate = owner ? useNavigate() : undefined;
        const now = Date.now();
        const key = name + hashKey(args);
        let cached = cache.get(key);
        let tracking;
        if (isServer) {
            const e = getRequestEvent();
            if (e) {
                const dataOnly = (e.router || (e.router = {})).dataOnly;
                if (dataOnly) {
                    const data = e && (e.router.data || (e.router.data = {}));
                    if (data && key in data)
                        return data[key];
                    if (Array.isArray(dataOnly) && !matchKey(key, dataOnly)) {
                        data[key] = undefined;
                        return Promise.resolve();
                    }
                }
            }
        }
        if (getListener() && !isServer) {
            tracking = true;
            onCleanup(() => cached[4].count--);
        }
        if (cached &&
            cached[0] &&
            (isServer ||
                intent === "native" ||
                cached[4].count ||
                Date.now() - cached[0] < PRELOAD_TIMEOUT)) {
            if (tracking) {
                cached[4].count++;
                cached[4][0](); // track
            }
            if (cached[3] === "preload" && intent !== "preload") {
                cached[0] = now;
            }
            let res = cached[1];
            if (intent !== "preload") {
                res =
                    "then" in cached[1]
                        ? cached[1].then(handleResponse(false), handleResponse(true))
                        : handleResponse(false)(cached[1]);
                !isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version
            }
            inPreloadFn && "then" in res && res.catch(() => { });
            return res;
        }
        let res;
        if (!isServer && sharedConfig.has && sharedConfig.has(key)) {
            res = sharedConfig.load(key); // hydrating
            // @ts-ignore at least until we add a delete method to sharedConfig
            delete globalThis._$HY.r[key];
        }
        else
            res = fn(...args);
        if (cached) {
            cached[0] = now;
            cached[1] = res;
            cached[3] = intent;
            !isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version
        }
        else {
            cache.set(key, (cached = [now, res, , intent, createSignal(now)]));
            cached[4].count = 0;
        }
        if (tracking) {
            cached[4].count++;
            cached[4][0](); // track
        }
        if (isServer) {
            const e = getRequestEvent();
            if (e && e.router.dataOnly)
                return (e.router.data[key] = res);
        }
        if (intent !== "preload") {
            res =
                "then" in res
                    ? res.then(handleResponse(false), handleResponse(true))
                    : handleResponse(false)(res);
        }
        inPreloadFn && "then" in res && res.catch(() => { });
        // serialize on server
        if (isServer &&
            sharedConfig.context &&
            sharedConfig.context.async &&
            !sharedConfig.context.noHydrate) {
            const e = getRequestEvent();
            (!e || !e.serverOnly) && sharedConfig.context.serialize(key, res);
        }
        return res;
        function handleResponse(error) {
            return async (v) => {
                if (v instanceof Response) {
                    const e = getRequestEvent();
                    if (e) {
                        for (const [key, value] of v.headers) {
                            if (key == "set-cookie")
                                e.response.headers.append("set-cookie", value);
                            else
                                e.response.headers.set(key, value);
                        }
                    }
                    const url = v.headers.get(LocationHeader);
                    if (url !== null) {
                        // client + server relative redirect
                        if (navigate && url.startsWith("/"))
                            startTransition(() => {
                                navigate(url, { replace: true });
                            });
                        else if (!isServer)
                            window.location.href = url;
                        else if (e)
                            e.response.status = 302;
                        return;
                    }
                    if (v.customBody)
                        v = await v.customBody();
                }
                if (error)
                    throw v;
                cached[2] = v;
                return v;
            };
        }
    });
    cachedFn.keyFor = (...args) => name + hashKey(args);
    cachedFn.key = name;
    return cachedFn;
}
query.get = (key) => {
    const cached = getCache().get(key);
    return cached[2];
};
query.set = (key, value) => {
    const cache = getCache();
    const now = Date.now();
    let cached = cache.get(key);
    if (cached) {
        cached[0] = now;
        cached[1] = Promise.resolve(value);
        cached[2] = value;
        cached[3] = "preload";
    }
    else {
        cache.set(key, (cached = [now, Promise.resolve(value), value, "preload", createSignal(now)]));
        cached[4].count = 0;
    }
};
query.delete = (key) => getCache().delete(key);
query.clear = () => getCache().clear();
/** @deprecated use query instead */
export const cache = query;
function matchKey(key, keys) {
    for (let k of keys) {
        if (k && key.startsWith(k))
            return true;
    }
    return false;
}
// Modified from the amazing Tanstack Query library (MIT)
// https://github.com/TanStack/query/blob/main/packages/query-core/src/utils.ts#L168
export function hashKey(args) {
    return JSON.stringify(args, (_, val) => isPlainObject(val)
        ? Object.keys(val)
            .sort()
            .reduce((result, key) => {
            result[key] = val[key];
            return result;
        }, {})
        : val);
}
function isPlainObject(obj) {
    let proto;
    return (obj != null &&
        typeof obj === "object" &&
        (!(proto = Object.getPrototypeOf(obj)) || proto === Object.prototype));
}
