// Caching for the gallery, keyed on the source that produced it.
//
// Building the gallery takes three minutes of GPU work, which is fine once and
// intolerable every time the page is opened. But a cache that has to be cleared
// by hand is worse than no cache: it will eventually show you last week's
// pixels while you are trying to judge this morning's change, and you will trust
// it because it looks like a render.
//
// So the key is a fingerprint of the code itself. Edit any file under src/ and
// the fingerprint changes, the old entry stops matching, and the gallery
// rebuilds without being asked. Nothing to remember and nothing to invalidate.
//
// Thumbnails are stored as WebP blobs in IndexedDB rather than raw pixels in
// localStorage: sixty-five scenes at six frames of 256x144 is about 57MB raw,
// which localStorage would refuse and IndexedDB would rather not hold either.
// Compressed it is a few megabytes.

const DB_NAME = 'flow-state-gallery';
const STORE = 'builds';
const DB_VERSION = 1;

/**
 * A fingerprint of every source file that can change what the gallery renders.
 *
 * `import.meta.glob` is resolved by Vite at build time, so this covers the
 * shaders, the identity, the look generator, the engine and the descriptors
 * without naming any of them — which matters, because the file that invalidates
 * a render is exactly the one nobody remembers to list.
 */
const SOURCES = import.meta.glob('/src/**/*.js', { query: '?raw', import: 'default', eager: true });

export function sourceFingerprint() {
    // Sorted, so the hash does not depend on glob iteration order.
    const paths = Object.keys(SOURCES).sort();
    let h = 0x811c9dc5 >>> 0;
    const mix = (str) => {
        for (let i = 0; i < str.length; i++) {
            h ^= str.charCodeAt(i);
            h = Math.imul(h, 0x01000193) >>> 0;
        }
    };
    for (const path of paths) {
        mix(path);
        mix(String(SOURCES[path]));
    }
    return `${paths.length}-${h.toString(16).padStart(8, '0')}`;
}

function openDb() {
    return new Promise((resolve, reject) => {
        const request = indexedDB.open(DB_NAME, DB_VERSION);
        request.onupgradeneeded = () => {
            const db = request.result;
            if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE);
        };
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
    });
}

function tx(db, mode, fn) {
    return new Promise((resolve, reject) => {
        const t = db.transaction(STORE, mode);
        const request = fn(t.objectStore(STORE));
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
    });
}

/** The cached build for this exact source, or null. */
export async function loadGallery(fingerprint) {
    try {
        const db = await openDb();
        const record = await tx(db, 'readonly', (store) => store.get(fingerprint));
        db.close();
        return record || null;
    } catch {
        // A debug page is not worth failing over a storage quota or a private
        // window that refuses IndexedDB. Fall through to rebuilding.
        return null;
    }
}

/**
 * Store a build, and drop every other one.
 *
 * Only the current source is ever wanted, and keeping stale builds around is how
 * a cache quietly grows to hundreds of megabytes of images nobody will look at.
 */
export async function saveGallery(fingerprint, payload) {
    try {
        const db = await openDb();
        const keys = await tx(db, 'readonly', (store) => store.getAllKeys());
        await tx(db, 'readwrite', (store) => {
            for (const key of keys) if (key !== fingerprint) store.delete(key);
            return store.put(payload, fingerprint);
        });
        db.close();
        return true;
    } catch {
        return false;
    }
}

export async function clearGallery() {
    try {
        const db = await openDb();
        await tx(db, 'readwrite', (store) => store.clear());
        db.close();
    } catch { /* nothing to clear */ }
}

/** Raw RGBA (bottom-up, as WebGL hands it over) to a compressed blob. */
export function pixelsToBlob(pixels, width, height) {
    const canvas = document.createElement('canvas');
    canvas.width = width;
    canvas.height = height;
    const ctx = canvas.getContext('2d');
    const image = ctx.createImageData(width, height);
    const row = width * 4;
    for (let y = 0; y < height; y++) {
        const src = (height - 1 - y) * row;
        image.data.set(pixels.subarray(src, src + row), y * row);
    }
    ctx.putImageData(image, 0, 0);
    return new Promise((resolve) => canvas.toBlob(resolve, 'image/webp', 0.85));
}

/** Draw a stored blob into a canvas, sized to it. */
export async function blobToCanvas(canvas, blob) {
    const bitmap = await createImageBitmap(blob);
    canvas.width = bitmap.width;
    canvas.height = bitmap.height;
    canvas.getContext('2d').drawImage(bitmap, 0, 0);
    bitmap.close();
}
