import { matrixCholeskySolver } from "../matrix/index.js";
import { addWeights } from "../utils/addWeights.js";
import { createSystemMatrix } from "../utils/createSystemMatrix.js";
import { calculateAdaptiveWeights } from "../utils/index.js";
import { xEnsureFloat64 } from "./xEnsureFloat64.js";
import { xMultiply } from "./xMultiply.js";
export function xWhittakerSmoother(yData, options = {}) {
    const { algorithm = 'thomas', ...restOptions } = options;
    if (algorithm === 'thomas') {
        return whittakerByThomas(yData, restOptions);
    }
    return whittakerByCholesky(yData, restOptions);
}
/**
 * @deprecated Use xWhittakerSmoother instead.
 * TODO: Remove in next major version.
 */
export const xWhitakerSmoother = xWhittakerSmoother;
/**
 * Computes the baseline points for the given data using an iterative smoothing algorithm.
 * @param yData - the input data array.
 * @param options - the options for baseline computation.
 * @returns the computed baseline points.
 */
function whittakerByCholesky(yData, options = {}) {
    const { lambda = 100, maxIterations = 100, tolerance = 1e-6, factorStd = 3, learningRate = 0.5, } = options;
    const size = yData.length;
    const { controlPoints, weights } = getWeightsAndControlPoints(yData, options);
    const prevBaseline = new Float64Array(size);
    let iteration = 0;
    let delta = Infinity;
    let baseline = xEnsureFloat64(yData);
    const { upperTriangularNonZeros, permutationEncodedArray } = createSystemMatrix(size, lambda);
    while (iteration < maxIterations && delta > tolerance) {
        const { leftHandSide, rightHandSide } = addWeights(upperTriangularNonZeros, yData, weights);
        const cho = matrixCholeskySolver(leftHandSide, size, permutationEncodedArray);
        if (!cho) {
            return baseline;
        }
        const newBaseline = cho(rightHandSide);
        //weights is updated inplace
        calculateAdaptiveWeights(yData, newBaseline, weights, {
            controlPoints,
            learningRate,
            factorStd,
        });
        delta = calculateDelta(newBaseline, prevBaseline, size);
        prevBaseline.set(newBaseline);
        baseline = xEnsureFloat64(newBaseline);
        iteration++;
    }
    return baseline;
}
/**
 * Calculates the delta between the current and previous baseline.
 * @param baseline - the current baseline array.
 * @param prevBaseline - the previous baseline array.
 * @param n - the length of the arrays.
 * @returns the calculated delta value.
 */
function calculateDelta(baseline, prevBaseline, n) {
    let sum = 0;
    for (let i = 0; i < n; i++) {
        sum += (baseline[i] - prevBaseline[i]) ** 2;
    }
    return Math.sqrt(sum / n);
}
/**
 * Retrieves the control points and weights for the given data, the weights are modified multiplication of controlPoints if it exists.
 * @param y - the input data array.
 * @param options - the options for control points and weights.
 * @returns the control points and modified weights.
 */
function getWeightsAndControlPoints(y, options = {}) {
    const { controlPoints } = options;
    const { weights = Float64Array.from({ length: y.length }).fill(1) } = options;
    if (controlPoints && controlPoints.length !== y.length) {
        throw new RangeError('controlPoints should match the length with X');
    }
    else if (weights.length !== y.length) {
        throw new RangeError('weights should match the length with X');
    }
    return {
        weights: controlPoints ? xMultiply(weights, controlPoints) : weights,
        controlPoints,
    };
}
function whittakerByThomas(yData, options = {}) {
    const { lambda = 100, maxIterations = 100, tolerance = 1e-6, factorStd = 3, learningRate = 0.5, } = options;
    const n = yData.length;
    const y = xEnsureFloat64(yData);
    const { controlPoints, weights } = getWeightsAndControlPoints(yData, options);
    const prevBaseline = new Float64Array(n);
    let baseline = xEnsureFloat64(yData);
    // Precompute base diagonal and constant off-diagonals (−lambda)
    const baseDiag = new Float64Array(n);
    if (n === 1) {
        baseDiag[0] = lambda;
    }
    else {
        baseDiag[0] = lambda;
        for (let i = 1; i < n - 1; i++)
            baseDiag[i] = 2 * lambda;
        baseDiag[n - 1] = lambda;
    }
    const lower = new Float64Array(Math.max(0, n - 1));
    const upper = new Float64Array(Math.max(0, n - 1));
    for (let i = 0; i < lower.length; i++) {
        lower[i] = -lambda;
        upper[i] = -lambda;
    }
    const main = new Float64Array(n);
    const rhs = new Float64Array(n);
    const solution = new Float64Array(n);
    const workC = new Float64Array(Math.max(0, n - 1));
    const workD = new Float64Array(n);
    let iteration = 0;
    let delta = Infinity;
    /**
     * Iterative Whittaker smoothing implementation using the Thomas algorithm
     * (tridiagonal solver). The method constructs the system
     * [W + lambda * D'D] z = W * y where D is the second-difference operator
     * and W are adaptive weights. At each iteration the tridiagonal system is
     * solved with the Thomas algorithm and the weights are updated using
     * `calculateAdaptiveWeights` until convergence.
     *
     * Use this algorithm by passing `algorithm: 'thomas'` to `xWhittakerSmoother`.
     * It is more memory-efficient for large inputs because it avoids forming
     * the full banded matrix, but both implementations aim to produce the same
     * baseline result.
     * @param yData - input signal/spectrum
     * @param options - smoothing options (supports `lambda`, `maxIterations`,
     *                  `tolerance`, `factorStd`, `learningRate`, and optional
     *                  `controlPoints` / `weights` inherited from
     *                  `CalculateAdaptiveWeightsOptions`).
     * @returns baseline as a `Float64Array`.
     */
    while (iteration < maxIterations && delta > tolerance) {
        // Build main diagonal = baseDiag + weights, and RHS = weights * y
        for (let i = 0; i < n; i++) {
            main[i] = baseDiag[i] + weights[i];
            rhs[i] = weights[i] * y[i];
        }
        // Solve tridiagonal system in-place into `solution`
        solveTridiagonalFloat64(lower, main, upper, rhs, solution, workC, workD);
        calculateAdaptiveWeights(y, solution, weights, {
            controlPoints,
            learningRate,
            factorStd,
        });
        delta = calculateDelta(solution, prevBaseline, n);
        prevBaseline.set(solution);
        baseline = xEnsureFloat64(solution);
        iteration++;
    }
    return baseline;
}
function solveTridiagonalFloat64(lower, diag, upper, rhs, out, cp, dp) {
    const n = diag.length;
    const x = out ?? new Float64Array(n);
    if (n === 0)
        return x;
    if (n === 1) {
        x[0] = rhs[0] / diag[0];
        return x;
    }
    const cprime = cp ?? new Float64Array(n - 1);
    const dprime = dp ?? new Float64Array(n);
    let denom = diag[0];
    cprime[0] = upper[0] / denom;
    dprime[0] = rhs[0] / denom;
    for (let i = 1; i < n - 1; i++) {
        denom = diag[i] - lower[i - 1] * cprime[i - 1];
        cprime[i] = upper[i] / denom;
        dprime[i] = (rhs[i] - lower[i - 1] * dprime[i - 1]) / denom;
    }
    denom = diag[n - 1] - lower[n - 2] * cprime[n - 2];
    dprime[n - 1] = (rhs[n - 1] - lower[n - 2] * dprime[n - 2]) / denom;
    x[n - 1] = dprime[n - 1];
    for (let i = n - 2; i >= 0; i--) {
        x[i] = dprime[i] - cprime[i] * x[i + 1];
    }
    return x;
}
//# sourceMappingURL=xWhittakerSmoother.js.map