// retoor <retoor@molodetz.nl>

export class ScrollMemory {
    constructor() {
        this.positionsKey = "dp-scroll:positions";
        this.trailKey = "dp-scroll:trail";
        this.intentKey = "dp-scroll:intent";
        this.maxEntries = 50;
        this.maxTrail = 20;
        this.maxAgeMs = 60 * 60 * 1000;
        this.intentAgeMs = 30 * 1000;
        this.restoreWindowMs = 4000;
        this.stableFramesNeeded = 10;
        this.saveDelayMs = 200;
        this.saveTimer = null;
        this.backSelector = "a.back-link, a[data-scroll-back], .breadcrumb a";
        if (!this.storageAvailable()) return;
        history.scrollRestoration = "manual";
        this.url = this.normalize(location.href);
        this.previousUrl = this.recordTrail();
        this.upgradeBackLinks();
        this.bindSave();
        this.bindIntent();
        window.addEventListener("pageshow", (e) => {
            if (!e.persisted) return;
            this.takeIntent();
            this.previousUrl = this.recordTrail();
        });
        this.restoreIfNeeded();
    }

    storageAvailable() {
        try {
            const probe = "dp-scroll:probe";
            sessionStorage.setItem(probe, "1");
            sessionStorage.removeItem(probe);
            return true;
        } catch {
            return false;
        }
    }

    normalize(href) {
        const url = new URL(href, location.href);
        return url.pathname + url.search;
    }

    readJson(key, fallback) {
        try {
            const raw = sessionStorage.getItem(key);
            if (!raw) return fallback;
            const value = JSON.parse(raw);
            return value === null || typeof value !== "object" ? fallback : value;
        } catch {
            return fallback;
        }
    }

    writeJson(key, value) {
        try {
            sessionStorage.setItem(key, JSON.stringify(value));
        } catch {}
    }

    positions() {
        return this.readJson(this.positionsKey, {});
    }

    prune(positions) {
        const now = Date.now();
        const entries = Object.entries(positions).filter(([, v]) => Array.isArray(v) && now - v[1] <= this.maxAgeMs);
        entries.sort((a, b) => b[1][1] - a[1][1]);
        return Object.fromEntries(entries.slice(0, this.maxEntries));
    }

    savePosition() {
        const y = Math.round(window.scrollY);
        const positions = this.prune(this.positions());
        if (y > 0) {
            positions[this.url] = [y, Date.now()];
        } else {
            delete positions[this.url];
        }
        this.writeJson(this.positionsKey, positions);
    }

    recordTrail() {
        const trail = this.readJson(this.trailKey, []);
        if (trail[trail.length - 1] !== this.url) trail.push(this.url);
        while (trail.length > this.maxTrail) trail.shift();
        this.writeJson(this.trailKey, trail);
        return trail.length > 1 ? trail[trail.length - 2] : null;
    }

    upgradeBackLinks() {
        if (!this.previousUrl) return;
        const previous = new URL(this.previousUrl, location.origin);
        document.querySelectorAll("a.back-link, a[data-scroll-back]").forEach((link) => {
            const href = link.getAttribute("href");
            if (!href) return;
            const target = new URL(href, location.href);
            if (target.origin !== location.origin || target.search || target.hash) return;
            if (target.pathname !== previous.pathname) return;
            if (this.normalize(target.href) === this.previousUrl) return;
            link.setAttribute("href", this.previousUrl);
        });
    }

    bindSave() {
        window.addEventListener("scroll", () => {
            if (this.saveTimer) return;
            this.saveTimer = setTimeout(() => {
                this.saveTimer = null;
                this.savePosition();
            }, this.saveDelayMs);
        }, { passive: true });
        window.addEventListener("pagehide", () => this.savePosition());
        document.addEventListener("visibilitychange", () => {
            if (document.hidden) this.savePosition();
        });
    }

    bindIntent() {
        document.addEventListener("click", (e) => {
            if (e.defaultPrevented || e.button !== 0) return;
            if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
            const link = e.target instanceof Element ? e.target.closest("a[href]") : null;
            if (!link || (link.target && link.target !== "_self")) return;
            const target = new URL(link.getAttribute("href"), location.href);
            if (target.origin !== location.origin) return;
            const destination = this.normalize(target.href);
            if (destination === this.url) return;
            if (!this.positions()[destination]) return;
            if (destination !== this.previousUrl && !link.matches(this.backSelector)) return;
            this.writeJson(this.intentKey, { url: destination, t: Date.now() });
        });
    }

    takeIntent() {
        const intent = this.readJson(this.intentKey, null);
        try {
            sessionStorage.removeItem(this.intentKey);
        } catch {}
        if (!intent || typeof intent.url !== "string") return null;
        if (Date.now() - (intent.t || 0) > this.intentAgeMs) return null;
        return intent.url;
    }

    restoreIfNeeded() {
        const intent = this.takeIntent();
        if (location.hash) return;
        const nav = performance.getEntriesByType("navigation")[0];
        const type = nav ? nav.type : "navigate";
        if (type !== "back_forward" && type !== "reload" && intent !== this.url) return;
        const saved = this.positions()[this.url];
        if (!saved) return;
        const [y, t] = saved;
        if (y <= 0 || Date.now() - t > this.maxAgeMs) return;
        this.applyScroll(y);
    }

    applyScroll(y) {
        const doc = document.scrollingElement || document.documentElement;
        const deadline = performance.now() + this.restoreWindowMs;
        const cancelEvents = ["wheel", "touchstart", "keydown", "pointerdown"];
        let cancelled = false;
        let stableFrames = 0;
        let lastHeight = 0;
        const cancel = () => { cancelled = true; };
        cancelEvents.forEach((name) => window.addEventListener(name, cancel, { once: true, passive: true }));
        const cleanup = () => cancelEvents.forEach((name) => window.removeEventListener(name, cancel));
        const step = () => {
            if (cancelled) {
                cleanup();
                return;
            }
            const limit = Math.max(0, doc.scrollHeight - window.innerHeight);
            const target = Math.min(y, limit);
            if (Math.abs(window.scrollY - target) > 1) this.scrollInstant(target);
            stableFrames = doc.scrollHeight === lastHeight && target === y ? stableFrames + 1 : 0;
            lastHeight = doc.scrollHeight;
            if (stableFrames >= this.stableFramesNeeded || performance.now() > deadline) {
                cleanup();
                return;
            }
            requestAnimationFrame(step);
        };
        step();
    }

    scrollInstant(top) {
        try {
            window.scrollTo({ top, left: 0, behavior: "instant" });
        } catch {
            window.scrollTo(0, top);
        }
    }
}
