
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'fs';

import { getOutputDirectory } from './config.js';
import { getPosterPath } from './poster.js';
import { needleLog } from './logging.js';

const pwaErrorWithInstructions = "It seems that you're trying to build a PWA using 'vite-plugin-pwa'!\nNeedle can manage PWA settings for you – just pass the same 'pwaOptions' to the needlePlugins and VitePWA plugins:\n\n1. Install the vite PWA plugin: npm install vite-plugin-pwa --save-dev\n\n2. Then update your vite.config.js:\n\n    import { VitePWA } from 'vite-plugin-pwa';\n    ...\n    needlePlugins(command, needleConfig, { pwa: pwaOptions }),\n    VitePWA(pwaOptions),\n\nIf you want to manage PWA building yourself and skip this check, please pass '{ pwa: false }' to needlePlugins.";

/** Provides reasonable defaults for a PWA manifest and workbox settings.
 * @param {"build" | "serve"} command
 * @param {import("../types/needleConfig").needleMeta | null} config
 * @param {import('../types').userSettings} userSettings
 * @returns {import('vite').Plugin | undefined}
 */
export function needlePWA(command, config, userSettings) {
    // @ts-ignore // TODO correctly type the userSettings.pwaOptions object
    /** @type {import("vite-plugin-pwa").VitePWAOptions | false} */
    const pwaOptions = userSettings.pwa === true
        ? { } // allow setting `pwa: true`
        : userSettings.pwa;

    /** The context contains files that are generated by the plugin and should be deleted after 
     * @type {import('../types').NeedlePWAProcessContext} */
    const context = { generatedFiles: [] }

    // early out, explicitly disabled
    if (pwaOptions === false) return;

    // no PWA options provided, but the vite-pwa plugin is enabled – inform that we can do better
    if (!pwaOptions) {
        return {
            name: "needle-pwa",
            apply: "build",
            configResolved(viteConfig) {
                if (findVitePWAPlugin(viteConfig)) {
                    errorThrow(pwaErrorWithInstructions);
                }
            },
        }
    }

    // @ts-ignore // TODO need an extra type so we can add more options into the VitePWAOptions object
    /** @type {number | undefined} */
    const updateInterval = pwaOptions.updateInterval || undefined;

    if ((command !== "build" && !pwaOptions?.devOptions?.enabled) || pwaOptions?.disable) return;

    if (!pwaOptions.registerType) {
        // log("Set PWA registerType to autoUpdate. This will automatically update the service worker when the content changes. If you want to manually update the service worker set the registerType to manual. See https://vite-pwa-org.netlify.app/guide/#configuring-vite-plugin-pwa");
        pwaOptions.registerType = "autoUpdate";
    }
    if (!pwaOptions.outDir) {
        const outDir = getOutputDirectory();
        // log("Set PWA outDir to " + outDir);
        pwaOptions.outDir = outDir;
    }

    const currentDir = process.cwd();

    /** @type {Array<string | Partial<import("vite-plugin-pwa").ManifestOptions>>} */
    const manifests = readdirSync(currentDir).filter(f => f.endsWith(".webmanifest")) || [];

    if (manifests.length > 1) {
        log("WARN: Multiple webmanifests found in web project directory. Only the first one will be processed: ", manifests[0]);
    }

    if (manifests.length <= 0 && !pwaOptions) {
        log("No webmanifests found in web project directory or in pwa options that are passed to the needle plugin (via `needlePlugin(command, needleConfig, { pwaOptions: PWAOptions })`)");
        return;
    }

    const customManifest = manifests.length > 0 ? manifests[0] : {};

    // ensure we have proper icons/name/description to match user settings
    processPWA(customManifest, context, pwaOptions, config, userSettings).catch((/** @type {unknown} */ e) => log("Error post processing PWA", customManifest, e));
    // ensures we have a valid workbox config
    processWorkboxConfig(pwaOptions);

    /** @type {Array<string>} */
    const postBuildMessages = [];

    // for debugging
    // log("PWA options", pwaOptions);

    return /** @type {import('vite').Plugin} */ ({
        name: 'needle:pwa',
        apply: 'build',
        enforce: "post",
        config(/** @type {{ plugins?: Array<Record<string,unknown>> }} */ viteConfig) {
            // Move the gzip plugin after PWA bundling
            let gzipPluginIndex = -1;
            let pwaPluginIndex = -1;
            /** @type {Record<string, unknown> | null} */
            let gzipPlugin = null;
            if (viteConfig.plugins) {
                for (let i = viteConfig.plugins.length - 1; i >= 0; i--) {
                    const plugin = viteConfig.plugins[i];
                    if (plugin && "name" in plugin && plugin.name === "vite:compression") {
                        gzipPluginIndex = i;
                        gzipPlugin = plugin;
                    }
                    if (plugin && "name" in plugin && plugin.name === "vite-plugin-pwa") {
                        pwaPluginIndex = i;
                    }
                }
                if (gzipPluginIndex >= 0 && gzipPluginIndex < viteConfig.plugins.length - 1) {
                    console.warn("[needle:pwa] vite compression plugin detected after PWA plugin. Moving it after the PWA plugin to avoid errors. Move the entry manually in vite.config to remove this warning.");
                    const gzipPlugin = viteConfig.plugins.splice(gzipPluginIndex, 1)[0];
                    const pwaPlugin = viteConfig.plugins[pwaPluginIndex];
                    const beforePwa = viteConfig.plugins.slice(0, pwaPluginIndex + 1);
                    const afterPwa = viteConfig.plugins.slice(pwaPluginIndex + 1);
                    viteConfig.plugins = [...beforePwa, pwaPlugin, gzipPlugin, ...afterPwa];
                }

                // Also add a number of filters – we want to avoid gzipping for specific files.
                // Note: this is current 
                /*
                if (gzipPlugin) {
                    const filteredFiles = [
                        "sw.js",
                        "needle.buildinfo.json",
                    ];
                    const method = gzipPlugin.filter;
                    if (!method) {
                        gzipPlugin.filter = (path) => {
                            console.log("PATH", path);
                            for (const file of filteredFiles) {
                                console.log("comparing ", path, "with", file);
                                if (path.endsWith(file)) return false;
                            }
                            return true;
                        }
                    }
                    else if (typeof method === "function") {
                        gzipPlugin.filter = (path) => {
                            for (const file of filteredFiles)
                                if (path.endsWith(file)) return false;
                            // check original function
                            return method(path);
                        }
                    }
                    else if (typeof method === "string") {
                        gzipPlugin.filter = (path) => {
                            for (const file of filteredFiles)
                                if (path.endsWith(file)) return false;
                            // check original regex
                            return !path.match(new RegExp(method));
                        }
                    }

                    console.log("[needle:pwa] Added filters to vite-plugin-compression to avoid gzipping service worker and build info files.", gzipPlugin);
                }
                */
            }
        },
        configResolved(config) {
            try {
                const plugin = findVitePWAPlugin(config);
                if (!plugin) {
                    errorThrow(pwaErrorWithInstructions);
                }

                // check if the index header contains the webmanifest ALSO
                // if yes then we want to log a warning at the end
                const indexPath = currentDir + "/index.html";
                if (existsSync(indexPath)) {
                    const indexContent = readFileSync(indexPath, 'utf8');
                    if (indexContent.includes(".webmanifest")) {
                        errorThrow("index.html contains a reference to a webmanifest. This is currently not supported. Please remove the reference from the index.html, or pass `{ pwa: false }` to needlePlugins to manage PWA building yourself.");
                    }
                }
            }
            catch (/** @type {unknown} */ err) {
                cleanup(context);
                throw err;
            }
        },
        transformIndexHtml: {
            order: 'pre',
            handler(html, _ctx) {
                // see https://vite-pwa-org.netlify.app/guide/auto-update.html
                // post transform so we want to linebreak after the vite logs
                console.log("\n");
                const scriptContent = `// 🌵 Added by the [needle-pwa] plugin.
import { registerSW } from 'virtual:pwa-register';
const debugServiceWorker = new URLSearchParams(window.location.search).get('debugpwa') !== null;
${updateInterval !== undefined ? `const intervalMS = ${updateInterval};` : ''}
let updateSW;
updateSW = registerSW({ 
    immediate: true,
    onRegisteredSW(swUrl, r) {
        // console.log('[needle-pwa] Service worker for PWA support has been registered.');
        ${updateInterval !== undefined ? `
        r && setInterval(async () => {
          if (debugServiceWorker) console.log('[needle-pwa] Checking for updates...');

          if (!(!r.installing && navigator)) {
            if (debugServiceWorker) console.log('[needle-pwa] Service worker is not ready yet');
            return;
          }
    
          // navigator.onLine is currently broken in Chrome
          /*
          if (('connection' in navigator) && !navigator.onLine) {
            // console.log('[needle-pwa] Service worker update check skipped – offline');
            return;
          }
          */

          const resp = await fetch(swUrl, {
            cache: 'no-store',
            headers: {
              'cache': 'no-store',
              'cache-control': 'no-cache',
            },
          });
    
          if (resp?.status === 200) {
            // if there's actually an update, onNeedRefresh will be called.
            await r.update();
          }
        }, intervalMS);
        ` : ``}
    },
    ${updateInterval !== undefined ? `
    onNeedRefresh() {
        if (debugServiceWorker) console.log('[needle-pwa] The web app has been updated in the background. Refreshing.');
        updateSW(true);
    },
    ` : ``}
    onOfflineReady() {
        if (debugServiceWorker) console.log('[needle-pwa] This web app is now installed and ready to be used offline.');
    },
});
`;
                log("Added PWA registration script to index.html." + (updateInterval !== undefined ? " Auto update interval is set to " + updateInterval + "ms." : ""));
                return {
                    html,
                    tags: [
                        {
                            tag: 'script',
                            children: scriptContent,
                            attrs: { type: "module" },
                            injectTo: 'head',
                        },
                    ]
                }
            }
        },
        closeBundle() {
            // copy the icons to the output directory
            const outputDir = getOutputDirectory();
            try {
                copyIcons(pwaOptions.manifest, outputDir);
            }
            catch (/** @type {unknown} */ e) {
                log("Error post processing PWA", e);
            }

            cleanup(context);

            for (const msg of postBuildMessages) {
                console.log(msg);
            }
        }
    })
}

/** Checks if the vite-plugin-pwa is present in the vite config
 * @param {import('vite').ResolvedConfig} config
 * @returns {import('vite-plugin-pwa').VitePWAOptions | null}
 */
function findVitePWAPlugin(config) {
    const plugins = /** @type {Array<Record<string,unknown>>} */ (config.plugins || []);
    /** @param {unknown} p
     * @returns {Record<string, unknown> | undefined}
     */
    function _findVitePWAPlugin(p) {
        if (Array.isArray(p))
            return /** @type {Array<unknown>} */ (p).find(_findVitePWAPlugin);
        if (/** @type {Record<string,unknown>} */(p)?.name === "vite-plugin-pwa")
            return /** @type {Record<string, unknown>} */ (p);
    }
    for (const plugin of plugins) {
        const foundVitePWAPlugin = _findVitePWAPlugin(plugin);
        if (foundVitePWAPlugin) return foundVitePWAPlugin;
    }
    return null;
}

/** @param {import('../types/index.d.ts').NeedlePWAProcessContext} context */
function cleanup(context) {
    for (const file of context.generatedFiles) {
        if (process.env.DEBUG) log("Cleanup generated file", file);
        rmSync(file);
    }
    context.generatedFiles.length = 0;
}

/** @param {...unknown} args */
function log(...args) {
    needleLog("needle-pwa", args.map(a => a != null && typeof a === 'object' ? JSON.stringify(a) : String(a ?? "")).join(" "), "log", { dimBody: false });
}

/** Throws an error with defined stacktrace. 
 * @param {string} message
 * @param {number} traceLimit How many stack frames to show in the error message
*/
function errorThrow(message, traceLimit = 0) {
    const { stackTraceLimit } = Error;
    Error.stackTraceLimit = traceLimit;
    const e = new Error("[needle-pwa] " + message);
    Error.stackTraceLimit = stackTraceLimit;
    throw e;
}

/** 
 * @param {string | Partial<import("vite-plugin-pwa").ManifestOptions>} webmanifestPath Path to the webmanifest file, or partial manifest itself
 * @param {import("../types/index.d.ts").NeedlePWAProcessContext} context
 * @param {import("vite-plugin-pwa").VitePWAOptions} pwaOptions
 * @param {import("../types/needleConfig").needleMeta | null} config
 * @param {import("../types/userconfig").userSettings | null} userSettings
 */
async function processPWA(webmanifestPath, context, pwaOptions, config, userSettings) {
    const outDir = getOutputDirectory();

    // if a path is provided, we read that and treat it as manifest
    // if an object is provided, we treat it as partial manifest

    /** @type {Partial<import("vite-plugin-pwa").ManifestOptions>} */
    let manifest = {};

    if (typeof webmanifestPath === "string") {
        manifest = JSON.parse(readFileSync(webmanifestPath, 'utf8'));
    }
    else {
        manifest = webmanifestPath || {};
    }

    processIcons(manifest, outDir, context);

    // TODO include assets in the manifest instead of manually copying
    // if (pwaOptions.includeAssets === undefined) pwaOptions.includeAssets = [];
    // pwaOptions.includeAssets = [...pwaOptions.includeAssets, ...manifest.icons?.map(i => i.src)];

    const packageJsonPath = process.cwd() + "/package.json";
    const packageJson = /** @type {{ name?: string, description?: string }} */ (existsSync(packageJsonPath) ? JSON.parse(readFileSync(packageJsonPath, 'utf8')) : {});

    const appName = config?.sceneName || packageJson.name || "Needle App";
    const packageName = typeof packageJson.name === "string" && packageJson.name.length > 0
        ? packageJson.name
        : appName;
    const packageNameSanitized = packageName
        .toLowerCase()
        .replace(/[^a-z0-9\/\-]/g, '')
        .replace("\/", ".");
    const description = typeof config?.meta === "string"
        ? config.meta : config?.meta?.description
        || packageJson.description
        || "Made with Needle Engine";

    /** @type {Partial<import("vite-plugin-pwa").ManifestOptions>} */
    const defaultManifest = {
        // Use the same title as in NeedleMeta
        name: appName,
        short_name: appName,
        id: "app.made-with-needle." + packageNameSanitized,
        description,
        start_url: "./index.html",
        theme_color: "#000000",
        background_color: "#000000",
        display: "standalone",
        display_override: [
            "window-controls-overlay",
            "standalone",
            "browser"
        ],
        dir: "ltr",
        lang: "en",
        related_applications: [],
        prefer_related_applications: false,
        categories: ["spatial", "3D", "needle", "webgl", "webxr", "xr"],
    };

    // Use the poster image if that exists
    if (!userSettings?.noPoster) {
        const posterPath = getPosterPath();
        defaultManifest.screenshots = [{
            src: "./" + posterPath,
            type: "image/webp",
            sizes: "1080x1080", // TODO get actual size
            form_factor: "wide",
        }];
    }

    // display override and window controls override, for native look and feel
    if (defaultManifest.display === undefined) defaultManifest.display = "standalone";
    if (defaultManifest.display_override === undefined) defaultManifest.display_override = [
        "window-controls-overlay",
        "standalone",
        "browser"
    ];

    // Merge into pwaOptions. start_url is important, and 
    // we also want to keep whatever is already in the pwaOptions.manifest object
    pwaOptions.manifest = {
        ...defaultManifest,
        ...manifest,
        ...pwaOptions.manifest
    };
}

/** Copies icons to the output directory  
 * If a start_url is provided, it will be used as the base path for relative icon paths
 * @param {Partial<import("vite-plugin-pwa").ManifestOptions>} manifest
 * @param {string} outDir
 * @param {import("../types/index.d.ts").NeedlePWAProcessContext} context
 * @returns {boolean} */
function processIcons(manifest, outDir, context) {
    let modified = false;

    if (!manifest.icons?.length) {
        // generate icons
        generateIcons(manifest, context);
    }

    if (!manifest.icons) return modified;

    for (let i = 0; i < manifest.icons?.length; i++) {
        try {
            const icon = manifest.icons[i];
            const src = icon.src;
            const iconSrcIsAbsolute = src.startsWith("http://") || src.startsWith("https://");
            const hasAbsoluteStartUrl = manifest.start_url?.startsWith("http://") || manifest.start_url?.startsWith("https://");
            if (!iconSrcIsAbsolute && hasAbsoluteStartUrl) {
                log("Making icon src relative to start_url", src);
                icon.src = manifest.start_url + src;
                modified = true;
            }
        }
        catch (/** @type {unknown} */ e) {
            log("Error processing PWA icon[" + i + "]", e);
        }
    }
    return modified;
}

/**
 * @param {Partial<import("vite-plugin-pwa").ManifestOptions>} manifest
 * @param {import("../types/index.d.ts").NeedlePWAProcessContext} context
 */
function generateIcons(manifest, context) {
    if (!manifest.icons) manifest.icons = [];
    if (process.env.DEBUG) log("Generating PWA icons");
    const sizes = [48, 128, 144, 192, 512];
    // Needle icon
    const defaultIconSVG = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 160 187.74"><defs><linearGradient id="a" x1="89.64" y1="184.81" x2="90.48" y2="21.85" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#62d399"/><stop offset=".51" stop-color="#acd842"/><stop offset=".9" stop-color="#d7db0a"/></linearGradient><linearGradient id="b" x1="69.68" y1="178.9" x2="68.08" y2="16.77" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0ba398"/><stop offset=".5" stop-color="#4ca352"/><stop offset="1" stop-color="#76a30a"/></linearGradient><linearGradient id="c" x1="36.6" y1="152.17" x2="34.7" y2="84.19" gradientUnits="userSpaceOnUse"><stop offset=".19" stop-color="#36a382"/><stop offset=".54" stop-color="#49a459"/><stop offset="1" stop-color="#76a30b"/></linearGradient><linearGradient id="d" x1="15.82" y1="153.24" x2="18" y2="90.86" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#267880"/><stop offset=".51" stop-color="#457a5c"/><stop offset="1" stop-color="#717516"/></linearGradient><linearGradient id="e" x1="135.08" y1="135.43" x2="148.93" y2="63.47" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b0d939"/><stop offset="1" stop-color="#eadb04"/></linearGradient><linearGradient id="f" x1="-4163.25" y1="2285.12" x2="-4160.81" y2="2215.34" gradientTransform="rotate(20 4088.49 13316.712)" gradientUnits="userSpaceOnUse"><stop offset=".17" stop-color="#74af52"/><stop offset=".48" stop-color="#99be32"/><stop offset="1" stop-color="#c0c40a"/></linearGradient><symbol id="g" viewBox="0 0 160 187.74"><path style="fill:url(#a)" d="M79.32 36.98v150.76L95 174.54l6.59-156.31-22.27 18.75z"/><path style="fill:url(#b)" d="M79.32 36.98 57.05 18.23l6.59 156.31 15.68 13.2V36.98z"/><path style="fill:url(#c)" d="m25.19 104.83 8.63 49.04 12.5-14.95-2.46-56.42-18.67 22.33z"/><path style="fill:url(#d)" d="M25.19 104.83 0 90.24l16.97 53.86 16.85 9.77-8.63-49.04z"/><path style="fill:#9c3" d="M43.86 82.5 18.69 67.98 0 90.24l25.18 14.59L43.86 82.5z"/><path style="fill:url(#e)" d="m134.82 78.69-9.97 56.5 15.58-9.04L160 64.1l-25.18 14.59z"/><path style="fill:url(#f)" d="m134.82 78.69-18.68-22.33-2.86 65 11.57 13.83 9.97-56.5z"/><path style="fill:#ffe113" d="m160 64.1-18.69-22.26-25.17 14.52 18.67 22.33L160 64.1z"/><path style="fill:#f3e600" d="M101.59 18.23 79.32 0 57.05 18.23l22.27 18.75 22.27-18.75z"/></symbol></defs><use width="160" height="187.74" xlink:href="#g"/></svg>`;
    const iconDir = process.cwd();

    const iconName = "pwa-icon-allSizes.svg";
    const iconPath = iconDir + "/" + iconName;
    writeFileSync(iconPath, defaultIconSVG, 'utf8');
    const iconSrc = "./" + iconName;
    // log("Generated PWA icon", iconSrc);
    const iconInfo = {
        src: iconSrc,
        type: "image/svg+xml",
        sizes: sizes.map(s => s + "x" + s).join(" "),
        "purpose": "any"
    };
    manifest.icons.push(iconInfo);
    context.generatedFiles.push(iconPath);
}

/** Tries to copy the icons to the output directory
 * TODO this should not be needed if we use pwaOptions.includeAssets
 * @param {Partial<import("vite-plugin-pwa").ManifestOptions>} manifest
 * @param {string} outDir
 */
function copyIcons(manifest, outDir) {
    for (let i = 0; i < manifest.icons?.length; i++) {
        try {
            const icon = manifest.icons[i];
            let src = icon.src;
            // if the icon starts with the start url we remove it (to get the output folder relative URL for copying)
            if (src.startsWith(manifest.start_url)) {
                src = src.substring(manifest.start_url.length);
            }
            // check again if the src is absolute
            const srcIsAbsolute = src.startsWith("http://") || src.startsWith("https://") || src.startsWith("//");
            if (srcIsAbsolute) {
                log("PWA icon src is absolute, not supported", icon.src);
                continue;
            }
            // src is relative -> we copy the file to the output directory
            const srcPath = process.cwd() + "/" + src;
            if (existsSync(srcPath)) {
                const targetPath = outDir + "/" + src;
                // if the icon already exists in the output directory we skip it
                if (existsSync(targetPath)) {
                    continue;
                }
                const outputDirectory = targetPath.substring(0, targetPath.lastIndexOf('/'));
                if (!existsSync(outputDirectory)) {
                    mkdirSync(outputDirectory, { recursive: true });
                }
                if (process.env.DEBUG) log("Copy PWA icon " + src + " to output");
                copyFileSync(srcPath, targetPath);
            }
        }
        catch (/** @type {unknown} */ e) {
            log("Error processing PWA icon[" + i + "]", e);
        }
    }
}



/**
 * Merges the current workbox config with the default workbox config
 * @param {import("vite-plugin-pwa").VitePWAOptions} manifest
 */
function processWorkboxConfig(manifest) {

    // Workaround: urlPattern, ignoreSearch und ignoreURLParametersMatching, dontCacheBustURLsMatching are because we currently append ?v=... to loaded GLB files
    const externalResourceCaching = {
        urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
        handler: 'CacheFirst',
        options: {
            cacheName: 'google-fonts-cache',
            expiration: {
                maxEntries: 10,
                maxAgeSeconds: 60 * 60 * 24 * 365 // <== 365 days
            },
            cacheableResponse: {
                statuses: [0, 200]
            }
        }
    };

    // this is our default config
    /** @type {Partial<import("workbox-build").GenerateSWOptions>} */
    const defaultWorkboxConfig = {
        globPatterns: ['**'],
        // glb files are large – we need to increase the cache size here.
        // In practice we want to precache everything for offline usage,
        // but we still want to get a warning beyond 50MB.
        maximumFileSizeToCacheInBytes: 50000000,
        dontCacheBustURLsMatching: /\.[a-f0-9]{8}\./,
        ignoreURLParametersMatching: [/.*/],
        additionalManifestEntries: [
            // Profile list
            { url: "https://immersive-web.github.io/webxr-input-profiles/packages/viewer/dist/profiles/profilesList.json", revision: "1" },
            // Quest 2
            { url: "https://immersive-web.github.io/webxr-input-profiles/packages/viewer/dist/profiles/oculus-touch-v3/profile.json", revision: "1" },
            { url: "https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0.16/dist/profiles/oculus-touch-v3/left.glb", revision: "1" },
            { url: "https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0.16/dist/profiles/oculus-touch-v3/right.glb", revision: "1" },
            // Quest 3
            { url: "https://immersive-web.github.io/webxr-input-profiles/packages/viewer/dist/profiles/meta-quest-touch-plus/profile.json", revision: "1" },
            { url: "https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0.16/dist/profiles/meta-quest-touch-plus/left.glb", revision: "1" },
            { url: "https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0.16/dist/profiles/meta-quest-touch-plus/right.glb", revision: "1" },
            // Hand tracking
            { url: "https://immersive-web.github.io/webxr-input-profiles/packages/viewer/dist/profiles/generic-hand/profile.json", revision: "1" },
            { url: "https://immersive-web.github.io/webxr-input-profiles/packages/viewer/dist/profiles/generic-hand/left.glb", revision: "1" },
            { url: "https://immersive-web.github.io/webxr-input-profiles/packages/viewer/dist/profiles/generic-hand/right.glb", revision: "1" },
        ],
        runtimeCaching: [
            // allow caching Google Fonts
            {
                ...externalResourceCaching, ...{
                    urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
                    options: { cacheName: 'google-fonts-cache' },
                }
            },
            // allow caching static resources from Google, like CSS
            {
                ...externalResourceCaching, ...{
                    urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
                    options: { cacheName: 'gstatic-fonts-cache' },
                }
            },
            // allow caching Needle cdn resources
            {
                ...externalResourceCaching, ...{
                    urlPattern: /^https:\/\/cdn\.needle\.tools\/.*/i,
                    handler: 'NetworkFirst',
                    options: { cacheName: 'needle-cdn-cache' },
                }
            },
            // allow caching controller resources, 
            // https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0/dist/profiles/
            {
                ...externalResourceCaching, ...{
                    urlPattern: /^https:\/\/cdn\.jsdelivr\.net\/npm\/@webxr-input-profiles\/assets@1\.0\/dist\/profiles\/.*/i,
                    handler: 'NetworkFirst',
                    options: { cacheName: 'webxr-controller-cache' },
                }
            },
            // allow caching local resources
            {
                urlPattern: (/** @type {{ url: URL }} */ { url }) => url,
                // Apply a network-first strategy.
                handler: 'NetworkFirst',
                options: {
                    matchOptions: {
                        ignoreSearch: true,
                    },
                }
            },
        ],
    }

    if (manifest.workbox) {
        manifest.workbox = { ...defaultWorkboxConfig, ...manifest.workbox };
        return true;
    }
    else {
        manifest.workbox = defaultWorkboxConfig;
        return true;
    }
}