/**
 * data-structure-typed
 *
 * @author Pablo Zeng
 * @copyright Copyright (c) 2022 Pablo Zeng <zrwusa@gmail.com>
 * @license MIT License
 */
import type { MatrixOptions } from '../../types';
import { ERR, raise } from '../../common';

/**
 *
 */
/**
 * Matrix — a numeric matrix with standard linear algebra operations.
 *
 * @example
 * // Basic matrix arithmetic
 *  const a = new Matrix([
 *       [1, 2],
 *       [3, 4]
 *     ]);
 *     const b = new Matrix([
 *       [5, 6],
 *       [7, 8]
 *     ]);
 *
 *     const sum = a.add(b);
 *     console.log(sum?.data); // [
 *  //      [6, 8],
 *  //      [10, 12]
 *  //    ];
 *
 *     const diff = b.subtract(a);
 *     console.log(diff?.data); // [
 *  //      [4, 4],
 *  //      [4, 4]
 *  //    ];
 * @example
 * // Matrix multiplication for transformations
 *  // 2x3 matrix * 3x2 matrix = 2x2 matrix
 *     const a = new Matrix([
 *       [1, 2, 3],
 *       [4, 5, 6]
 *     ]);
 *     const b = new Matrix([
 *       [7, 8],
 *       [9, 10],
 *       [11, 12]
 *     ]);
 *
 *     const product = a.multiply(b);
 *     console.log(product?.rows); // 2;
 *     console.log(product?.cols); // 2;
 *     // Row 0: 1*7+2*9+3*11=58, 1*8+2*10+3*12=64
 *     // Row 1: 4*7+5*9+6*11=139, 4*8+5*10+6*12=154
 *     console.log(product?.data); // [
 *  //      [58, 64],
 *  //      [139, 154]
 *  //    ];
 * @example
 * // Matrix transpose (square matrix)
 *  const m = new Matrix([
 *       [1, 2, 3],
 *       [4, 5, 6],
 *       [7, 8, 9]
 *     ]);
 *
 *     const transposed = m.transpose();
 *     console.log(transposed.rows); // 3;
 *     console.log(transposed.cols); // 3;
 *     console.log(transposed.data); // [
 *  //      [1, 4, 7],
 *  //      [2, 5, 8],
 *  //      [3, 6, 9]
 *  //    ];
 *
 *     // Transpose of transpose = original
 *     console.log(transposed.transpose().data); // m.data;
 * @example
 * // Get and set individual cells
 *  const m = new Matrix([
 *       [0, 0, 0],
 *       [0, 0, 0]
 *     ]);
 *
 *     m.set(0, 1, 42);
 *     m.set(1, 2, 99);
 *
 *     console.log(m.get(0, 1)); // 42;
 *     console.log(m.get(1, 2)); // 99;
 *     console.log(m.get(0, 0)); // 0;
 *
 *     // Out of bounds returns undefined
 *     console.log(m.get(5, 5)); // undefined;
 */
export class Matrix {
  /**
   * The constructor function initializes a matrix object with the provided data and options, or with
   * default values if no options are provided.
   * @param {number[][]} data - A 2D array of numbers representing the data for the matrix.
   * @param [options] - The `options` parameter is an optional object that can contain the following
   * properties:
   */
  constructor(data: number[][], options?: MatrixOptions) {
    if (options) {
      const { rows, cols, addFn, subtractFn, multiplyFn } = options;
      if (typeof rows === 'number' && rows > 0) this._rows = rows;
      else this._rows = data.length;
      if (typeof cols === 'number' && cols > 0) this._cols = cols;
      else this._cols = data[0]?.length || 0;
      if (addFn) this._addFn = addFn;
      if (subtractFn) this._subtractFn = subtractFn;
      if (multiplyFn) this._multiplyFn = multiplyFn;
    } else {
      this._rows = data.length;
      this._cols = data[0]?.length ?? 0;
    }

    if (data.length > 0) {
      this._data = data;
    } else {
      this._data = [];
      for (let i = 0; i < this.rows; i++) {
        this._data[i] = new Array(this.cols).fill(0);
      }
    }
  }

  protected _rows: number = 0;

  /**
   * The function returns the number of rows.
   * @returns The number of rows.
   */
  get rows(): number {
    return this._rows;
  }

  protected _cols: number = 0;

  /**
   * The function returns the value of the protected variable _cols.
   * @returns The number of columns.
   */
  get cols(): number {
    return this._cols;
  }

  protected _data: number[][];

  /**
   * The function returns a two-dimensional array of numbers.
   * @returns The data property, which is a two-dimensional array of numbers.
   */
  get data(): number[][] {
    return this._data;
  }

  /**
   * The above function returns the value of the _addFn property.
   * @returns The value of the property `_addFn` is being returned.
   */
  get addFn() {
    return this._addFn;
  }

  /**
   * The function returns the value of the _subtractFn property.
   * @returns The `_subtractFn` property is being returned.
   */
  get subtractFn() {
    return this._subtractFn;
  }

  /**
   * The function returns the value of the _multiplyFn property.
   * @returns The `_multiplyFn` property is being returned.
   */
  get multiplyFn() {
    return this._multiplyFn;
  }

  /**
   * The `get` function returns the value at the specified row and column index if it is a valid index.
   * @param {number} row - The `row` parameter represents the row index of the element you want to
   * retrieve from the data array.
   * @param {number} col - The parameter "col" represents the column number of the element you want to
   * retrieve from the data array.
   * @returns The `get` function returns a number if the provided row and column indices are valid.
   * Otherwise, it returns `undefined`.
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
    * @example
 * // Get and set individual cells
 *  const m = new Matrix([
 *       [0, 0, 0],
 *       [0, 0, 0]
 *     ]);
 *
 *     m.set(0, 1, 42);
 *     m.set(1, 2, 99);
 *
 *     console.log(m.get(0, 1)); // 42;
 *     console.log(m.get(1, 2)); // 99;
 *     console.log(m.get(0, 0)); // 0;
 *
 *     // Out of bounds returns undefined
 *     console.log(m.get(5, 5)); // undefined;
   */
  get(row: number, col: number): number | undefined {
    if (this.isValidIndex(row, col)) {
      return this.data[row][col];
    }
  }

  /**
   * The set function updates the value at a specified row and column in a two-dimensional array.
   * @param {number} row - The "row" parameter represents the row index of the element in a
   * two-dimensional array or matrix. It specifies the row where the value will be set.
   * @param {number} col - The "col" parameter represents the column index of the element in a
   * two-dimensional array.
   * @param {number} value - The value parameter represents the number that you want to set at the
   * specified row and column in the data array.
   * @returns a boolean value. It returns true if the index (row, col) is valid and the value is
   * successfully set in the data array. It returns false if the index is invalid and the value is not
   * set.
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
    * @example
 * // Modify individual cells
 *  const m = Matrix.zeros(2, 2);
 *     console.log(m.set(0, 0, 5)); // true;
 *     console.log(m.set(1, 1, 10)); // true;
 *     console.log(m.get(0, 0)); // 5;
 *     console.log(m.get(1, 1)); // 10;
   */
  set(row: number, col: number, value: number): boolean {
    if (this.isValidIndex(row, col)) {
      this.data[row][col] = value;
      return true;
    }
    return false;
  }

  /**
   * The function checks if the dimensions of the given matrix match the dimensions of the current
   * matrix.
   * @param {Matrix} matrix - The parameter `matrix` is of type `Matrix`.
   * @returns a boolean value.
   */
  isMatchForCalculate(matrix: Matrix): boolean {
    return this.rows === matrix.rows && this.cols === matrix.cols;
  }

  /**
   * The `add` function adds two matrices together, returning a new matrix with the result.
   * @param {Matrix} matrix - The `matrix` parameter is an instance of the `Matrix` class.
   * @returns The `add` method returns a new `Matrix` object that represents the result of adding the
   * current matrix with the provided `matrix` parameter.
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
    * @example
 * // Basic matrix arithmetic
 *  const a = new Matrix([
 *       [1, 2],
 *       [3, 4]
 *     ]);
 *     const b = new Matrix([
 *       [5, 6],
 *       [7, 8]
 *     ]);
 *
 *     const sum = a.add(b);
 *     console.log(sum?.data); // [
 *  //      [6, 8],
 *  //      [10, 12]
 *  //    ];
 *
 *     const diff = b.subtract(a);
 *     console.log(diff?.data); // [
 *  //      [4, 4],
 *  //      [4, 4]
 *  //    ];
   */
  add(matrix: Matrix): Matrix | undefined {
    if (!this.isMatchForCalculate(matrix)) {
      raise(Error, ERR.matrixDimensionMismatch('addition'));
    }

    const resultData: number[][] = [];
    for (let i = 0; i < this.rows; i++) {
      resultData[i] = [];
      for (let j = 0; j < this.cols; j++) {
        const a = this.get(i, j),
          b = matrix.get(i, j);
        if (a !== undefined && b !== undefined) {
          resultData[i][j] = this._addFn(a, b) ?? 0;
        }
      }
    }

    return new Matrix(resultData, {
      rows: this.rows,
      cols: this.cols,
      addFn: this.addFn,
      subtractFn: this.subtractFn,
      multiplyFn: this.multiplyFn
    });
  }

  /**
   * The `subtract` function performs element-wise subtraction between two matrices and returns a new
   * matrix with the result.
   * @param {Matrix} matrix - The `matrix` parameter is an instance of the `Matrix` class. It
   * represents the matrix that you want to subtract from the current matrix.
   * @returns a new Matrix object with the result of the subtraction operation.
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
    * @example
 * // Element-wise subtraction
 *  const a = Matrix.from([[5, 6], [7, 8]]);
 *     const b = Matrix.from([[1, 2], [3, 4]]);
 *     const result = a.subtract(b);
 *     console.log(result?.toArray()); // [[4, 4], [4, 4]];
   */
  subtract(matrix: Matrix): Matrix | undefined {
    if (!this.isMatchForCalculate(matrix)) {
      raise(Error, ERR.matrixDimensionMismatch('subtraction'));
    }

    const resultData: number[][] = [];
    for (let i = 0; i < this.rows; i++) {
      resultData[i] = [];
      for (let j = 0; j < this.cols; j++) {
        const a = this.get(i, j),
          b = matrix.get(i, j);
        if (a !== undefined && b !== undefined) {
          resultData[i][j] = this._subtractFn(a, b) ?? 0;
        }
      }
    }

    return new Matrix(resultData, {
      rows: this.rows,
      cols: this.cols,
      addFn: this.addFn,
      subtractFn: this.subtractFn,
      multiplyFn: this.multiplyFn
    });
  }

  /**
   * The `multiply` function performs matrix multiplication between two matrices and returns the result
   * as a new matrix.
   * @param {Matrix} matrix - The `matrix` parameter is an instance of the `Matrix` class.
   * @returns a new Matrix object.
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
    * @example
 * // Matrix multiplication for transformations
 *  // 2x3 matrix * 3x2 matrix = 2x2 matrix
 *     const a = new Matrix([
 *       [1, 2, 3],
 *       [4, 5, 6]
 *     ]);
 *     const b = new Matrix([
 *       [7, 8],
 *       [9, 10],
 *       [11, 12]
 *     ]);
 *
 *     const product = a.multiply(b);
 *     console.log(product?.rows); // 2;
 *     console.log(product?.cols); // 2;
 *     // Row 0: 1*7+2*9+3*11=58, 1*8+2*10+3*12=64
 *     // Row 1: 4*7+5*9+6*11=139, 4*8+5*10+6*12=154
 *     console.log(product?.data); // [
 *  //      [58, 64],
 *  //      [139, 154]
 *  //    ];
   */
  multiply(matrix: Matrix): Matrix | undefined {
    if (this.cols !== matrix.rows) {
      raise(Error, ERR.matrixDimensionMismatch('multiplication (A.cols must equal B.rows)'));
    }

    const resultData: number[][] = [];
    for (let i = 0; i < this.rows; i++) {
      resultData[i] = [];
      for (let j = 0; j < matrix.cols; j++) {
        let sum: number | undefined;
        for (let k = 0; k < this.cols; k++) {
          const a = this.get(i, k),
            b = matrix.get(k, j);
          if (a !== undefined && b !== undefined) {
            const multiplied = this.multiplyFn(a, b);
            if (multiplied !== undefined) {
              sum = this.addFn(sum, multiplied);
            }
          }
        }
        if (sum !== undefined) resultData[i][j] = sum;
      }
    }

    return new Matrix(resultData, {
      rows: this.rows,
      cols: matrix.cols,
      addFn: this.addFn,
      subtractFn: this.subtractFn,
      multiplyFn: this.multiplyFn
    });
  }

  /**
   * The transpose function takes a matrix and returns a new matrix that is the transpose of the
   * original matrix.
   * @returns The transpose() function returns a new Matrix object with the transposed data.
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
    * @example
 * // Matrix transpose (square matrix)
 *  const m = new Matrix([
 *       [1, 2, 3],
 *       [4, 5, 6],
 *       [7, 8, 9]
 *     ]);
 *
 *     const transposed = m.transpose();
 *     console.log(transposed.rows); // 3;
 *     console.log(transposed.cols); // 3;
 *     console.log(transposed.data); // [
 *  //      [1, 4, 7],
 *  //      [2, 5, 8],
 *  //      [3, 6, 9]
 *  //    ];
 *
 *     // Transpose of transpose = original
 *     console.log(transposed.transpose().data); // m.data;
   */
  transpose(): Matrix {
    if (this.data.some(row => row.length !== this.cols)) {
      raise(Error, ERR.matrixNotRectangular());
    }

    const resultData: number[][] = [];

    for (let j = 0; j < this.cols; j++) {
      resultData[j] = [];
      for (let i = 0; i < this.rows; i++) {
        const trans = this.get(i, j);
        if (trans !== undefined) resultData[j][i] = trans;
      }
    }

    return new Matrix(resultData, {
      rows: this.cols,
      cols: this.rows,
      addFn: this.addFn,
      subtractFn: this.subtractFn,
      multiplyFn: this.multiplyFn
    });
  }

  /**
   * The `inverse` function calculates the inverse of a square matrix using Gaussian elimination.
   * @returns a Matrix object, which represents the inverse of the original matrix.
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
    * @example
 * // Compute the inverse of a 2x2 matrix
 *  const m = Matrix.from([[4, 7], [2, 6]]);
 *     const inv = m.inverse();
 *     console.log(inv); // defined;
 *     // A * A^-1 should ≈ Identity
 *     const product = m.multiply(inv!);
 *     console.log(product?.get(0, 0)); // toBeCloseTo;
 *     console.log(product?.get(0, 1)); // toBeCloseTo;
 *     console.log(product?.get(1, 0)); // toBeCloseTo;
 *     console.log(product?.get(1, 1)); // toBeCloseTo;
   */
  inverse(): Matrix | undefined {
    // Check if the matrix is square
    if (this.rows !== this.cols) {
      raise(Error, ERR.matrixNotSquare());
    }

    // Create an augmented matrix [this | I]
    const augmentedMatrixData: number[][] = [];
    for (let i = 0; i < this.rows; i++) {
      augmentedMatrixData[i] = this.data[i].slice(); // Copy the original matrix
      for (let j = 0; j < this.cols; j++) {
        augmentedMatrixData[i][this.cols + j] = i === j ? 1 : 0; // Append the identity matrix
      }
    }

    const augmentedMatrix = new Matrix(augmentedMatrixData, {
      rows: this.rows,
      cols: this.cols * 2,
      addFn: this.addFn,
      subtractFn: this.subtractFn,
      multiplyFn: this.multiplyFn
    });

    // Apply Gaussian elimination to transform the left half into the identity matrix
    for (let i = 0; i < this.rows; i++) {
      // Find pivot
      let pivotRow = i;
      while (pivotRow < this.rows && augmentedMatrix.get(pivotRow, i) === 0) {
        pivotRow++;
      }

      if (pivotRow === this.rows) {
        // Matrix is singular, and its inverse does not exist
        raise(Error, ERR.matrixSingular());
      }

      // Swap rows to make the pivot the current row
      augmentedMatrix._swapRows(i, pivotRow);

      // Scale the pivot row to make the pivot element 1
      const pivotElement = augmentedMatrix.get(i, i) ?? 1;

      if (pivotElement === 0) {
        // Handle division by zero
        raise(Error, ERR.matrixSingular());
      }

      augmentedMatrix._scaleRow(i, 1 / pivotElement);

      // Eliminate other rows to make elements in the current column zero
      for (let j = 0; j < this.rows; j++) {
        if (j !== i) {
          let factor = augmentedMatrix.get(j, i);
          if (factor === undefined) factor = 0;

          augmentedMatrix._addScaledRow(j, i, -factor);
        }
      }
    }

    // Extract the right half of the augmented matrix as the inverse
    const inverseData: number[][] = [];
    for (let i = 0; i < this.rows; i++) {
      inverseData[i] = augmentedMatrix.data[i].slice(this.cols);
    }

    return new Matrix(inverseData, {
      rows: this.rows,
      cols: this.cols,
      addFn: this.addFn,
      subtractFn: this.subtractFn,
      multiplyFn: this.multiplyFn
    });
  }

  /**
   * The dot function calculates the dot product of two matrices and returns a new matrix.
   * @param {Matrix} matrix - The `matrix` parameter is an instance of the `Matrix` class.
   * @returns a new Matrix object.
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
    * @example
 * // Dot product of two matrices
 *  const a = Matrix.from([[1, 2], [3, 4]]);
 *     const b = Matrix.from([[5, 6], [7, 8]]);
 *     const result = a.dot(b);
 *     console.log(result?.toArray()); // [[19, 22], [43, 50]];
   */
  dot(matrix: Matrix): Matrix | undefined {
    if (this.cols !== matrix.rows) {
      raise(Error, ERR.matrixDimensionMismatch('dot product (A.cols must equal B.rows)'));
    }

    const resultData: number[][] = [];
    for (let i = 0; i < this.rows; i++) {
      resultData[i] = [];
      for (let j = 0; j < matrix.cols; j++) {
        let sum: number | undefined;
        for (let k = 0; k < this.cols; k++) {
          const a = this.get(i, k),
            b = matrix.get(k, j);
          if (a !== undefined && b !== undefined) {
            const multiplied = this.multiplyFn(a, b);
            if (multiplied !== undefined) {
              sum = this.addFn(sum, multiplied);
            }
          }
        }
        if (sum !== undefined) resultData[i][j] = sum;
      }
    }

    return new Matrix(resultData, {
      rows: this.rows,
      cols: matrix.cols,
      addFn: this.addFn,
      subtractFn: this.subtractFn,
      multiplyFn: this.multiplyFn
    });
  }

  /**
   * The function checks if a given row and column index is valid within a specified range.
   * @param {number} row - The `row` parameter represents the row index of a two-dimensional array or
   * matrix. It is a number that indicates the specific row in the matrix.
   * @param {number} col - The "col" parameter represents the column index in a two-dimensional array
   * or grid. It is used to check if the given column index is valid within the bounds of the grid.
   * @returns A boolean value is being returned.
   */
  isValidIndex(row: number, col: number): boolean {
    return row >= 0 && row < this.rows && col >= 0 && col < this.cols;
  }

  /**
   * The `clone` function returns a new instance of the Matrix class with the same data and properties
   * as the original instance.
   * @returns The `clone()` method is returning a new instance of the `Matrix` class with the same data
   * and properties as the current instance.
   */
  clone(): Matrix {
    return new Matrix(
      this._data.map(row => [...row]),
      {
        rows: this.rows,
        cols: this.cols,
        addFn: this.addFn,
        subtractFn: this.subtractFn,
        multiplyFn: this.multiplyFn
      }
    );
  }

  // ─── Standard interface ─────────────────────────────────────

  /**
   * Returns [rows, cols] dimensions tuple.
   */
  get size(): [number, number] {
    return [this._rows, this._cols];
  }

  isEmpty(): boolean {
    return this._rows === 0 || this._cols === 0;
  }

  /**
   * Returns a deep copy of the data as a plain 2D array.
   */
  toArray(): number[][] {
    return this._data.map(row => [...row]);
  }

  /**
   * Returns a flat row-major array.
   */
  flatten(): number[] {
    const result: number[] = [];
    for (const row of this._data) {
      for (const v of row) result.push(v);
    }
    return result;
  }

  /**
   * Iterates over rows.
   */
  [Symbol.iterator](): IterableIterator<number[]> {
    const data = this._data;
    let i = 0;
    return {
      [Symbol.iterator]() {
        return this;
      },
      next(): IteratorResult<number[]> {
        if (i < data.length) {
          return { value: [...data[i++]], done: false };
        }
        return { value: undefined, done: true } as IteratorResult<number[]>;
      }
    };
  }

  /**
   * Visits each element with its row and column index.
   */
  forEach(callback: (value: number, row: number, col: number) => void): void {
    for (let i = 0; i < this._rows; i++) {
      for (let j = 0; j < this._cols; j++) {
        callback(this._data[i][j], i, j);
      }
    }
  }

  /**
   * Maps each element (number → number) and returns a new Matrix.
   */
  map(callback: (value: number, row: number, col: number) => number): Matrix {
    const resultData: number[][] = [];
    for (let i = 0; i < this._rows; i++) {
      resultData[i] = [];
      for (let j = 0; j < this._cols; j++) {
        resultData[i][j] = callback(this._data[i][j], i, j);
      }
    }
    return new Matrix(resultData, {
      rows: this._rows,
      cols: this._cols,
      addFn: this.addFn,
      subtractFn: this.subtractFn,
      multiplyFn: this.multiplyFn
    });
  }

  print(): void {
    for (const row of this._data) {
      console.log(row.join('\t'));
    }
  }

  // ─── Factory methods ────────────────────────────────────────

  /**
   * Creates a rows×cols zero matrix.
   * @example
   * ```ts
   * const z = Matrix.zeros(2, 3); // [[0,0,0],[0,0,0]]
   * ```
   */
  static zeros(rows: number, cols: number): Matrix {
    const data: number[][] = Array.from({ length: rows }, () => new Array(cols).fill(0));
    return new Matrix(data);
  }

  /**
   * Creates an n×n identity matrix.
   * @example
   * ```ts
   * const I = Matrix.identity(3); // [[1,0,0],[0,1,0],[0,0,1]]
   * ```
   */
  static identity(n: number): Matrix {
    const data: number[][] = Array.from({ length: n }, (_, i) =>
      Array.from({ length: n }, (_, j) => (i === j ? 1 : 0))
    );
    return new Matrix(data);
  }

  /**
   * Creates a Matrix from a plain 2D array (deep copy).
   * @example
   * ```ts
   * const m = Matrix.from([[1, 2], [3, 4]]);
   * m.get(0, 1); // 2
   * ```
   */
  static from(data: number[][]): Matrix {
    return new Matrix(data.map(row => [...row]));
  }

  protected _addFn(a: number | undefined, b: number): number | undefined {
    if (a === undefined) return b;
    return a + b;
  }

  protected _subtractFn(a: number, b: number) {
    return a - b;
  }

  protected _multiplyFn(a: number, b: number) {
    return a * b;
  }

  /**
   * The function `_swapRows` swaps the positions of two rows in an array.
   * @param {number} row1 - The `row1` parameter is the index of the first row that you want to swap.
   * @param {number} row2 - The `row2` parameter is the index of the second row that you want to swap
   * with the first row.
   */
  protected _swapRows(row1: number, row2: number): void {
    const temp = this.data[row1];
    this.data[row1] = this.data[row2];
    this.data[row2] = temp;
  }

  /**
   * The function scales a specific row in a matrix by a given scalar value.
   * @param {number} row - The `row` parameter represents the index of the row in the matrix that you
   * want to scale. It is a number that indicates the position of the row within the matrix.
   * @param {number} scalar - The scalar parameter is a number that is used to multiply each element in
   * a specific row of a matrix.
   */
  protected _scaleRow(row: number, scalar: number): void {
    for (let j = 0; j < this.cols; j++) {
      let multiplied = this.multiplyFn(this.data[row][j], scalar);
      if (multiplied === undefined) multiplied = 0;
      this.data[row][j] = multiplied;
    }
  }

  /**
   * The function `_addScaledRow` multiplies a row in a matrix by a scalar value and adds it to another
   * row.
   * @param {number} targetRow - The targetRow parameter represents the index of the row in which the
   * scaled values will be added.
   * @param {number} sourceRow - The sourceRow parameter represents the index of the row from which the
   * values will be scaled and added to the targetRow.
   * @param {number} scalar - The scalar parameter is a number that is used to scale the values in the
   * source row before adding them to the target row.
   */
  protected _addScaledRow(targetRow: number, sourceRow: number, scalar: number): void {
    for (let j = 0; j < this.cols; j++) {
      let multiplied = this.multiplyFn(this.data[sourceRow][j], scalar);
      if (multiplied === undefined) multiplied = 0;
      const scaledValue = multiplied;
      let added = this.addFn(this.data[targetRow][j], scaledValue);
      if (added === undefined) added = 0;
      this.data[targetRow][j] = added;
    }
  }
}
