let running = false;
let timeoutId = null;

const SELECTOR = "[data-kd-timer='1']";

const configs = new WeakMap(); // Element -> parsed config
const elements = new Set();    // active elements (registered)
const visible = new Set();     // visible elements (intersection observer)

let mo = null;
let io = null;

// Pre-create formatters to avoid per-tick allocations.
const intFormat = new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 });

function pad2(n) { return n < 10 ? "0" + n : "" + n; }

function truncSec(ms) {
    // floor toward zero for display purposes
    return ms >= 0 ? Math.floor(ms / 1000) : -Math.floor(Math.abs(ms) / 1000);
}

function parseConfig(el) {
    // Read dataset once, convert to numbers/booleans once.
    // If attributes change, we'll re-parse via mutation observer.
    const ds = el.dataset;

    const kind = ds.kind || "countdown"; // "elapsed" | "countdown"
    const format = ds.format || "seconds"; // "minutes" | "seconds" | "totalseconds"
    const negative = ds.negative === "1";

    const startMs = ds.startMs ? Number(ds.startMs) : 0;
    const endMs = ds.endMs ? Number(ds.endMs) : 0;

    const minLabel = ds.minLabel || "Min";
    const lessThanAMin = ds.lessThanAMin || "< 1 Min";

    // Optional: stop updating after done if negative disabled (can reduce work a lot)
    const stopWhenDone = ds.stopWhenDone === "1"; // default false

    return { kind, format, negative, startMs, endMs, minLabel, lessThanAMin, stopWhenDone };
}

function ensureConfig(el) {
    let cfg = configs.get(el);
    if (!cfg) {
        cfg = parseConfig(el);
        configs.set(el, cfg);
    }
    return cfg;
}

function formatElapsed(ms) {
    if (ms <= 0) return "";

    const totalSeconds = Math.floor(ms / 1000);
    const s = totalSeconds % 60;
    const totalMinutes = Math.floor(totalSeconds / 60);
    const m = totalMinutes % 60;
    const h = Math.floor(totalMinutes / 60);

    if (h < 1) return `${m}:${pad2(s)}`;
    return `${h}:${pad2(m)}:${pad2(s)}`;
}

function formatCountdownSeconds(ms, negativeEnabled) {
    if (!negativeEnabled && ms <= 0) return "0:00";

    const neg = ms < 0;
    const absSeconds = Math.floor(Math.abs(ms) / 1000);
    const s = absSeconds % 60;
    const totalMinutes = Math.floor(absSeconds / 60);
    const m = totalMinutes % 60;
    const h = Math.floor(totalMinutes / 60);

    const prefix = (neg && negativeEnabled) ? "-" : "";

    if (h >= 1) return `${prefix}${h}:${pad2(m)}:${pad2(s)}`;
    return `${prefix}${m}:${pad2(s)}`;
}

function formatCountdownMinutes(ms, minLabel, lessThanAMin) {
    if (ms <= 0) return `0 ${minLabel}`;
    if (ms <= 60_000) return lessThanAMin;

    const mins = Math.floor(ms / 60_000);
    return `${mins} ${minLabel}`;
}

function formatCountdownTotalSeconds(ms) {
    if (ms <= 0) return "0";
    return intFormat.format(Math.floor(ms / 1000));
}

function updateElement(el, now) {
    const cfg = ensureConfig(el);

    if (cfg.kind === "elapsed") {
        const text = formatElapsed(now - cfg.startMs);
        if (el.textContent !== text) el.textContent = text;
        return;
    }

    // countdown
    const remaining = cfg.endMs - now;

    if (cfg.stopWhenDone && !cfg.negative && remaining <= 0) {
        // Final clamp display then stop tracking (big perf win if many timers finish)
        const finalText = cfg.format === "minutes"
            ? `0 ${cfg.minLabel}`
            : cfg.format === "totalseconds"
                ? "0"
                : "0:00";

        if (el.textContent !== finalText) el.textContent = finalText;

        // Remove from active updates
        elements.delete(el);
        visible.delete(el);
        return;
    }

    let text;
    if (cfg.format === "minutes") {
        text = formatCountdownMinutes(remaining, cfg.minLabel, cfg.lessThanAMin);
    } else if (cfg.format === "totalseconds") {
        text = formatCountdownTotalSeconds(remaining);
    } else {
        text = formatCountdownSeconds(remaining, cfg.negative);
    }

    if (el.textContent !== text) el.textContent = text;
}

function updateAll() {
    const now = Date.now();

    // Update only visible timers if IO is available; otherwise update all.
    const targetSet = (io ? visible : elements);

    // Batch DOM writes in one frame to reduce layout thrash
    requestAnimationFrame(() => {
        for (const el of targetSet) {
            updateElement(el, now);
        }
    });
}

function scheduleNextTick() {
    if (!running) return;

    updateAll();

    const now = Date.now();
    const next = (Math.floor(now / 1000) + 1) * 1000;
    const delay = Math.max(1, next - now);

    timeoutId = setTimeout(scheduleNextTick, delay);
}

function register(el) {
    if (!(el instanceof Element)) return;
    if (!el.matches(SELECTOR)) return;

    elements.add(el);
    configs.delete(el); // force parse fresh

    if (io) io.observe(el);
}

function unregister(el) {
    elements.delete(el);
    visible.delete(el);
    configs.delete(el);
    if (io) io.unobserve(el);
}

function scanInitial() {
    document.querySelectorAll(SELECTOR).forEach(register);
}

function setupMutationObserver() {
    mo = new MutationObserver(mutations => {
        for (const m of mutations) {
            // New nodes
            m.addedNodes?.forEach(n => {
                if (!(n instanceof Element)) return;
                if (n.matches(SELECTOR)) register(n);
                n.querySelectorAll?.(SELECTOR).forEach(register);
            });

            // Removed nodes
            m.removedNodes?.forEach(n => {
                if (!(n instanceof Element)) return;
                if (n.matches(SELECTOR)) unregister(n);
                n.querySelectorAll?.(SELECTOR).forEach(unregister);
            });

            // Attribute changes: re-parse config
            if (m.type === "attributes" && m.target instanceof Element && m.target.matches(SELECTOR)) {
                configs.delete(m.target);
            }
        }
    });

    mo.observe(document.documentElement, {
        subtree: true,
        childList: true,
        attributes: true,
        attributeFilter: ["data-start-ms", "data-end-ms", "data-format", "data-negative", "data-min-label", "data-less-than-a-min", "data-kind", "data-stop-when-done"]
    });
}

function setupIntersectionObserver() {
    if (!("IntersectionObserver" in window)) return;

    io = new IntersectionObserver(entries => {
        for (const e of entries) {
            if (e.isIntersecting) visible.add(e.target);
            else visible.delete(e.target);
        }
    }, { root: null, threshold: 0 });

    // Observe any already registered elements
    for (const el of elements) io.observe(el);
}

export function start() {
    if (running) return;
    running = true;

    scanInitial();
    setupIntersectionObserver();
    setupMutationObserver();

    scheduleNextTick();
}

export function stop() {
    running = false;

    if (timeoutId) {
        clearTimeout(timeoutId);
        timeoutId = null;
    }

    if (mo) { mo.disconnect(); mo = null; }
    if (io) { io.disconnect(); io = null; }

    elements.clear();
    visible.clear();
    // configs is WeakMap; no need to clear
}

export function refreshNow() {
    updateAll();
}
