import { Matrix } from 'ml-matrix';

import type { Data2D, ParameterizedFunction } from './types.ts';

/**
 * Difference of the matrix function over the parameters
 * @param data Array of points to fit in the format [x1, x2, ... ], [y1, y2, ... ]
 * @param evaluatedData - Array of previous evaluated function values
 * @param params - Array of previous parameter values
 * @param gradientDifference - The step size to approximate the jacobian matrix
 * @param centralDifference - If true the jacobian matrix is approximated by central differences otherwise by forward differences
 * @param paramFunction - The parameters and returns a function with the independent variable as a parameter
 */
export default function gradientFunction(
  data: Data2D,
  evaluatedData: Float64Array,
  params: number[],
  gradientDifference: number[],
  paramFunction: ParameterizedFunction,
  centralDifference: boolean,
): Matrix {
  const nbParams = params.length;
  const nbPoints = data.x.length;
  const ans = Matrix.zeros(nbParams, nbPoints);

  let rowIndex = 0;
  for (let param = 0; param < nbParams; param++) {
    if (gradientDifference[param] === 0) continue;
    let delta = gradientDifference[param];
    let auxParams = params.slice();
    auxParams[param] += delta;
    const funcParam = paramFunction(auxParams);
    if (!centralDifference) {
      for (let point = 0; point < nbPoints; point++) {
        ans.set(
          rowIndex,
          point,
          (evaluatedData[point] - funcParam(data.x[point])) / delta,
        );
      }
    } else {
      auxParams = params.slice();
      auxParams[param] -= delta;
      delta *= 2;
      const funcParam2 = paramFunction(auxParams);
      for (let point = 0; point < nbPoints; point++) {
        ans.set(
          rowIndex,
          point,
          (funcParam2(data.x[point]) - funcParam(data.x[point])) / delta,
        );
      }
    }
    rowIndex++;
  }

  return ans;
}
