/*
 * shared/diff.js — a small, dependency-free text diff.
 *
 * Attaches to window.zddc.diff. Must load AFTER shared/zddc.js (which
 * creates the window.zddc object). Used by the browse tool's markdown
 * version-history viewer to show what changed between any two saved
 * versions; kept in shared/ so other tools can reuse it.
 *
 * API:
 *   window.zddc.diff.lines(oldStr, newStr)
 *     → [{ type: 'eq'|'del'|'add', text }]   line-level diff (LCS)
 *   window.zddc.diff.words(oldStr, newStr)
 *     → [{ type: 'eq'|'del'|'add', text }]   token-level diff for one
 *       changed line (whitespace-preserving), for intra-line highlights
 *   window.zddc.diff.stats(ops) → { added, removed }
 *
 * The line diff trims the common prefix/suffix before running the O(n*m)
 * LCS dynamic program, so a small edit in a large file stays cheap. A
 * safety cap falls back to "replace whole block" when the changed middle
 * is pathologically large, so the UI never freezes.
 */
(function () {
    'use strict';

    var LCS_CELL_CAP = 4000000; // ~4M cells (n*m) before the fallback

    function splitLines(s) {
        return String(s == null ? '' : s).replace(/\r\n/g, '\n').split('\n');
    }

    // LCS diff of two arrays of strings → ordered [{type, text}] ops.
    function lcsDiff(a, b) {
        var n = a.length, m = b.length;
        if (n === 0 && m === 0) return [];
        if (n === 0) return b.map(function (t) { return { type: 'add', text: t }; });
        if (m === 0) return a.map(function (t) { return { type: 'del', text: t }; });

        if (n * m > LCS_CELL_CAP) {
            // Too large to diff finely without risking a UI stall: treat
            // the whole block as a wholesale replacement.
            var out = a.map(function (t) { return { type: 'del', text: t }; });
            return out.concat(b.map(function (t) { return { type: 'add', text: t }; }));
        }

        // dp[i][j] = LCS length of a[i:] and b[j:].
        var dp = new Array(n + 1);
        for (var i = 0; i <= n; i++) dp[i] = new Array(m + 1).fill(0);
        for (var ii = n - 1; ii >= 0; ii--) {
            for (var jj = m - 1; jj >= 0; jj--) {
                if (a[ii] === b[jj]) dp[ii][jj] = dp[ii + 1][jj + 1] + 1;
                else dp[ii][jj] = Math.max(dp[ii + 1][jj], dp[ii][jj + 1]);
            }
        }

        var ops = [], i = 0, j = 0;
        while (i < n && j < m) {
            if (a[i] === b[j]) { ops.push({ type: 'eq', text: a[i] }); i++; j++; }
            else if (dp[i + 1][j] >= dp[i][j + 1]) { ops.push({ type: 'del', text: a[i] }); i++; }
            else { ops.push({ type: 'add', text: b[j] }); j++; }
        }
        while (i < n) ops.push({ type: 'del', text: a[i++] });
        while (j < m) ops.push({ type: 'add', text: b[j++] });
        return ops;
    }

    function diffLines(oldStr, newStr) {
        var a = splitLines(oldStr), b = splitLines(newStr);
        var ops = [];

        // Common prefix.
        var start = 0;
        while (start < a.length && start < b.length && a[start] === b[start]) start++;
        // Common suffix (not overlapping the prefix).
        var endA = a.length, endB = b.length;
        while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) { endA--; endB--; }

        for (var p = 0; p < start; p++) ops.push({ type: 'eq', text: a[p] });
        var mid = lcsDiff(a.slice(start, endA), b.slice(start, endB));
        for (var k = 0; k < mid.length; k++) ops.push(mid[k]);
        for (var s = endA; s < a.length; s++) ops.push({ type: 'eq', text: a[s] });
        return ops;
    }

    // Whitespace-preserving tokenization: words and the runs of
    // whitespace between them are separate tokens, so a re-diff lines up
    // on word boundaries while keeping the original spacing renderable.
    function tokenize(s) {
        return String(s == null ? '' : s).split(/(\s+)/).filter(function (x) { return x !== ''; });
    }

    function diffWords(oldStr, newStr) {
        return lcsDiff(tokenize(oldStr), tokenize(newStr));
    }

    function stats(ops) {
        var added = 0, removed = 0;
        for (var i = 0; i < ops.length; i++) {
            if (ops[i].type === 'add') added++;
            else if (ops[i].type === 'del') removed++;
        }
        return { added: added, removed: removed };
    }

    if (!window.zddc) {
        throw new Error('shared/diff.js: window.zddc must be loaded first');
    }
    window.zddc.diff = { lines: diffLines, words: diffWords, stats: stats };
})();
