const EPSILON = 1e-10;

/**
 * Perform QR factorization in-place.
 * Subfunction that analyzes eigenvalues by QR decomposition.
 *
 * Single-shift Wilkinson iteration with LAPACK-style exceptional shifts:
 * deflates the trailing subdiagonal one row at a time as it converges. A
 * complex eigenvalue pair leaves a 2×2 trailing block whose inner subdiagonal
 * does not vanish — only safe today for matrices with all-real eigenvalues.
 * If convergence fails within 30·n sweeps (LAPACK convention), the routine
 * throws rather than spinning.
 *
 * @see http://www-in.aut.ac.jp/~minemura/pub/Csimu/C/QRmethod.html
 * @param {number[]|Float64Array} a Square matrix, column-major flat layout (a[j*n + i] = row i, col j); modified in place (R is left on the upper triangle)
 * @param {number} n size of the matrix in single dimension
 */
export function matrix_qr_in_place(a, n) {
    const q = new Float64Array(n * n);
    const w = new Float64Array(n);

    let mu, wa, sinx, cosx, sum1, sum2;
    let m = n;

    let i, j, k;

    // LAPACK convention: ~30 QR sweeps per eigenvalue. Hitting this means
    // we're stuck (typically a complex eigenvalue pair the single-shift can't
    // deflate) — fail loudly rather than spin forever.
    const MAX_SWEEPS = 30 * n;
    let sweeps = 0;
    // Sweeps since the last deflation. Drives exceptional-shift fallbacks
    // when the standard Wilkinson shift stalls (typical pathology: a trailing
    // block with near-symmetric ±λ eigenvalues — the shift oscillates and
    // single-shift QR converges only linearly without a perturbation).
    let sweeps_since_deflation = 0;

    while (m > 1) {
        // a[m-1, m-2] in column-major: (m-2)*n + (m-1)
        const a10_index = (m - 2) * n + (m - 1);

        const a10 = a[a10_index];

        // Relative convergence test (LAPACK xLAHQR convention): subdiagonal
        // is "small" when it falls below EPSILON × the magnitude of the
        // surrounding 2×2 diagonal. Scales correctly with matrix magnitude.
        // Falls back to the absolute EPSILON when both diagonal entries
        // collapse to zero, so we don't refuse to deflate a zero block.
        const a00_abs = Math.abs(a[(m - 2) * n + (m - 2)]);
        const a11_abs = Math.abs(a[(m - 1) * n + (m - 1)]);
        const diagScale = a00_abs + a11_abs;
        const convTol = diagScale > 0 ? EPSILON * diagScale : EPSILON;

        if (Math.abs(a10) <= convTol) {
            m = m - 1;
            sweeps_since_deflation = 0;
            continue;
        }

        if (++sweeps > MAX_SWEEPS) {
            throw new Error(`matrix_qr_in_place: failed to converge after ${MAX_SWEEPS} sweeps (likely complex eigenvalue pair; single-shift QR only handles real spectra)`);
        }
        sweeps_since_deflation++;

        const a00 = a[(m - 2) * n + (m - 2)];
        const a01 = a[(m - 1) * n + (m - 2)];
        const a11 = a[(m - 1) * n + (m - 1)];

        if (sweeps_since_deflation === 10 || sweeps_since_deflation === 20) {
            // Exceptional Rayleigh-quotient shift. The standard Wilkinson
            // shift can stall on a trailing 2×2 with near-symmetric (±λ)
            // eigenvalues: it keeps picking whichever root happens to be
            // closer to a11 that sweep, and the QR step doesn't make
            // progress. Falling back to mu = a11 alone breaks the symmetry
            // and gets convergence moving again (LAPACK xLAHQR does an
            // analogous fallback for the double-shift case at iter 10/20).
            mu = a11;
        } else {
            // Standard Wilkinson shift from the trailing 2×2 block.
            sum1 = a00 + a11;
            sum2 = a00 * a11 - a01 * a10;

            wa = sum1 * sum1 - 4.0 * sum2;
            if (wa < 0.0) {
                wa = 0.0;
            } else {
                wa = Math.sqrt(wa);
            }

            // Match the sign of wa to sum1 so we form the root with the
            // larger magnitude first; otherwise sum1 < 0, sum2 ≈ 0 makes
            // lam1 cancel to zero and lam2 = 0/0 = NaN poisons mu. Guard
            // the divide too for the degenerate sum1=0, sum2=0 case.
            const lam1 = 0.5 * (sum1 + (sum1 >= 0 ? wa : -wa));
            const lam2 = lam1 !== 0 ? sum2 / lam1 : 0;

            if (Math.abs(a11 - lam1) < Math.abs(a11 - lam2)) {
                mu = a11 - lam1;
            } else {
                mu = a11 - lam2;
            }
        }

        // Subtract mu from diagonal
        for (i = 0; i < m; i++) {
            a[i * n + i] -= mu;
        }

        //   QR decomposition, A becomes R
        for (i = 0; i < m * m; i++) {
            q[i] = 0.0;
        }
        for (i = 0; i < m; i++) {
            q[i * m + i] = 1.0;
        }

        for (i = 0; i < m - 1; i++) {
            // diagonal a[i, i] at i*n + i; sub-diagonal a[i+1, i] at i*n + (i+1)
            const diag = a[i * n + i];
            const subdiag = a[i * n + i + 1];

            sum1 = Math.sqrt(diag * diag + subdiag * subdiag);

            if (sum1 < EPSILON) {
                // Both diag and subdiag ≈ 0 — column already zero in these
                // two rows. Rotation is the identity (NOT the zero matrix,
                // which would annihilate rows i and i+1 across all j > i).
                sinx = 0.0;
                cosx = 1.0;
            } else {
                sinx = subdiag / sum1;
                cosx = diag / sum1;
            }

            // Rotate rows i and i+1 of A for columns j > i. In column-major,
            // those two row entries within column j live at adjacent offsets
            // (j*n + i, j*n + i+1) — contiguous within each column.
            for (j = i + 1; j < m; j++) {
                const jOff = j * n;
                const a_ij = a[jOff + i];
                const a_i1j = a[jOff + i + 1];
                sum2 = a_ij * cosx + a_i1j * sinx;
                a[jOff + i + 1] = -a_ij * sinx + a_i1j * cosx;
                a[jOff + i] = sum2;
            }

            a[i * n + i + 1] = 0.0;
            a[i * n + i] = sum1;

            // Accumulate the rotation into Q. In the original (row-major) the
            // inner loop walked j over rows of q, touching (q[j, i], q[j, i+1])
            // — which is two columns of q. In column-major those columns are
            // contiguous: i*m..i*m+m-1 and (i+1)*m..(i+1)*m+m-1.
            const qiOff = i * m;
            const qi1Off = (i + 1) * m;
            for (j = 0; j < m; j++) {
                const q_ji = q[qiOff + j];
                const q_ji1 = q[qi1Off + j];
                sum2 = q_ji * cosx + q_ji1 * sinx;
                q[qi1Off + j] = -q_ji * sinx + q_ji1 * cosx;
                q[qiOff + j] = sum2;
            }
        }

        // A := R * Q. Cache row i of R into w (since R is upper-triangular
        // we only need k >= i), then compute the new row i of A. With
        // column-major a, reading row i is strided — accept it; m is small.
        for (i = 0; i < m; i++) {
            for (j = i; j < m; j++) {
                w[j] = a[j * n + i];
            }

            for (j = 0; j < m; j++) {
                sum1 = 0.0;

                const qjOff = j * m;
                for (k = i; k < m; k++) {
                    sum1 += w[k] * q[qjOff + k];
                }

                a[j * n + i] = sum1;
            }
        }

        // Add mu back to diagonal
        for (i = 0; i < m; i++) {
            a[i * n + i] += mu;
        }
    }
}
