Matrix_Functions_Lib_JD
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © jorisduyck
//@version=5

library("Matrix_Functions_Lib_JD")
//@description This library converts 1D Pine arrays into 2D matrices and provides matrix manupulation and calculation functions.

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// The arrays provided in Pinescript are linear 1D strucures that can be seen either as a large vertical stack or
// a horizontal row containing a list of values, colors, bools,..
// 
// With the FUNCTIONS in this script the 1D ARRAY LIST can be CONVERTED INTO A 2D MATRIX form
// 
// 
///////////////////////////////////////////
/// BASIC INFO ON THE MATRIX STRUCTURE: ///
///////////////////////////////////////////
// 
// The matrix is set up as an 2D structure and is devided in ROWS and COLUMNS.
//  following the standard mathematical notation:
//
//      a 3 x 4 matrix =          4 columns
//                                 0 1 2 3  column index
//                              0 [a b c d]
//                       3 rows 1 [e f g h]
//                              2 [i j k l]
//                            row
//                           index
//
// With the use of some purpose-built functions, values can be placed or retrieved in a specific column of a certain row
//  this can be done by intuitively using row_nr and column_nr coördinates,
//  without having to worry on what exact index of the Pine array this value is located (the functions do these conversions for you)
//
//
// the syntax I propose for the 2D Matrix array has the following structure:
// 
//  - the array starts with 2 VALUES describing the DIMENSION INFORMATION, (rows, columns)
//      these are ignored in the actual calculations and serve as a metadata header (similar to the "location, time,... etc." data that is stored in photo files)
//      so the array always carries it's own info about the nr. of rows and columns and doesn't need is seperate "info" file!
//
//      To stay consistent with the standard Pinescript (array and [lookback]) indexing:
//      - indexes for sheets and columns start from 0 (first) and run up to the (total nr of sheets or columns) - 1
//      - indexes for rows also start from 0 (most recent, cfr.[lookback]) and run up to the (total nr of rows) - 1
// 
//  - this 2 value metadata header is followed by the actual df data
//      the actual data array can consist of (100,000 - 2) usable items,
//      
//      In a theoretical example, you can have a matrix with almost 20,000 rows with each 5 columns of data (eg. open, high, low, close, volume) in it!!!
//
///////////////////////////////////
/// SCHEMATIC OF THE STRUCTURE: ///
///////////////////////////////////
//
////// (metadata header with dimensions info)
//
//                (0)             (1)            (array index)
//       [nr_of_rows/records, nr_of_columns,
//
//
////// (actual matrix array data)
//  
//                  0             1             2               3                   column_index
//
//                 (2)           (3)           (4)             (5)            (array index)
//        0    record0_val0, record0_val1, record0_val2, record0_val3, ...,
//      
//                 (6)           (7)           (8)             (9)            (array index)
//        1    record1_val0, record1_val1, record1_val2, record1_val3, ...,
//      
//                 (10)          (11)          (12)            (13)            (array index)
//        2    record2_val0, record2_val1, record2_val2, record2_val3, ...,
//      
//        3    ...
//
// row_index
//
////////////////////////////////////
/// DATA MANIPULATION FUNCTIONS: ///
////////////////////////////////////
//
// A set of functions are implemented to write info to and retrieve info from the dataframe.
//  Of course the possibilities are not limited to the functions here, these were written because they were needed for the script!
//  A whole list of other functions can be easily written for other manipulation purposes.
//  The ones written in the script are 2D versions of most of the functions that are provided for normal 1D arrays.
//
//  Feel free to use the functions library below in your scripts! A little shoutout would be nice, of course!!
//
//
///////////////////////////////////////////////////////////////////////
/// LIST OF FUNCTIONS contained in this script:                     ///
/// (many other can of course be written, using the same structure) ///
///////////////////////////////////////////////////////////////////////
//
// - INFO functions
//      * get the number of COLUMNS
//      * get the number of ROWS
//      * get the total SIZE (number of columns, number of rows)
// 
// - INITIALISATION and SET functions (commands to build the 2D matrix)
//      * MATRIX INITIALISATION function, builds 2D matrix with dimensional metadata/size in first 2 values //
//          a new matrix array is built (cfr. the conventions I propose above) containing a certain nr of rows and columns
//      * function to SET a single value in a certain row/lookback period and column
//
// - GET functions (to retrieve info from the 2D matrix)
//      * function to GET/retrieve a single VALUE from a certain row/lookback period and column
//      * function to cut of the metadata header and return the array body (as an array)
//      * function to GET/retrieve the values from a ROW/RECORD from a certain row/lookback period, the values are returned as an array
//
// - 1D - 2D COORDINATE CONVERSION FUNCTIONS (handy with for loop indexes) ///
//      * function to get the ROW INDEX in a 2D matrix from the 1D array index
//      * function to get the COLUMN INDEX in a 2D matrix from the 1D array index
//      * function to get (row, column) coordinates in a 2D matrix from the 1D array index
//      * function to get the 1D array index from (row, column) coordinates in a 2D matrix
//
// - Matrix MANIPULATION functions
//      * function to ADD a ROW/RECORD on the TOP of the array, shift the whole list one down and REMOVE the OLDEST row/record
//          (2D version of "unshift" + "pop" but with a whole row at once)
//      * function to REMOVE one or more ROWS/RECORDS from a 2D matrix
//          (if from_row == to_row, only this row is removed)
//      * function to REMOVE one or more COLUMNS from a 2D matrix
//          (if from_column == to_column, only this column is removed)
//      * function to INSERT an ARRAY of ROWS/RECORDS at a certain row number in a 2D matrix
//      * function to INSERT an ARRAY of COLUMNS at a certain column number in a 2D matrix
//      * function to APPEND/ADD an ARRAY of ROWS/RECORDS to the BOTTOM of a 2D matrix
//      * function to APPEND/ADD an ARRAY of COLUMNS to the SIDE of a 2D matrix
//      * function to POP/REMOVE and return the last ROW/RECORD from the BOTTOM of a 2D matrix
//      * function to POP/REMOVE and return the last column from the SIDE of a 2D matrix
//
// - function to print a matrix
//      This function is mainly used for debugging purposes and displays the array as a 2D matrix notation
//
//
// Enjoy!
// JD.
//
// #NotTradingAdvice #DYOR
//
//  Disclaimer.
// I AM NOT A FINANCIAL ADVISOR.
// THESE IDEAS ARE NOT ADVICE AND ARE FOR EDUCATION PURPOSES ONLY.
// ALWAYS DO YOUR OWN RESEARCH!
//}

// Function declarations //
//{
/////////////////////////////////////////////////////////////////////////////////
///                             Matrix Functions                              ///
/////////////////////////////////////////////////////////////////////////////////
//{

/// MATRIX INFO FUNCTIONS ///
//{
// @function Returns the number of rows from a 2D matrix
export get_nr_of_rows(float[] m) =>
//{
    _rows = int(array.get(m, 0))
    _rows
//}

// @function Returns the number of columns from a 2D matrix
export get_nr_of_columns(float[] m) =>
//{
    _columns = int(array.get(m, 1))
    _columns
//}

// @function Returns a tuple with the total number of rows and columns from a 2D matrix
export get_size(float[] m) =>
//{
    _rows    = int(array.get(m, 0))
    _columns = int(array.get(m, 1))
    [_rows, _columns]
//}
//}

/// INITIALISATION AND SET FUNCTIONS ///
//{
// @function 2D matrix init function, builds a 2D matrix with dimensional metadata in first two values and fills it with a default value, the body of the actual matrix data starts at index 2.
export init(int nr_of_rows, int nr_of_columns, float fill = 0) =>
//{
    size = int(nr_of_rows * nr_of_columns)
    m    = array.new_float(size + 2, fill)
    array.set(m, 0, nr_of_rows), array.set(m, 1, nr_of_columns)
    m
//}

// @function 2D matrix init function, builds a 2D matrix from an existing array by adding dimensional metadata in first two values, the body of the actual matrix data consists of the data of the source array and starts at index 2.
export from_list(int nr_of_rows, int nr_of_columns, float[] list) =>
//{
    size = int(nr_of_rows * nr_of_columns)
    m    = array.new_float(size + 2, 0)
    array.set(m, 0, nr_of_rows), array.set(m, 1, nr_of_columns)
    if array.size(list) <= array.size(m) - 2
        for i = 0 to array.size(list) - 1
            array.set(m, i + 2, array.get(list, i))
    m
//}

// @function Sets values in 2D matrix with (row index, column index) (index for rows and columns both starts at 0 !!)
export set(float[] m, int row_index = 0, int column_index = 0, float val = 0) =>
//{
    _columns = get_nr_of_columns(m)
    array.set(m, 2 + int((row_index * _columns) + column_index), val)
//}

// @function Fills all elements in a 2D matrix with a value
export fill_val(float[] m, float val = 0) =>
//{
    array.fill(m, val, 2)
//}

// @function Fills a 2D matrix with random values//
export randomize(float[] m, float min_range = 0, float max_range = 1) =>
//{
    [_rows, _columns] = get_size(m)
    for i = 0  to _rows - 1
        for j = 0  to _columns - 1
            _rnd_value = math.random(min_range, max_range)
            set(m, i, j, _rnd_value)
    m
//}
//}

/// GET FUNCTIONS ///
//{
// @function Gets values from 2D matrix with (row index, column index) (index for rows and columns both starts at 0 !!)
export get(float[] m, int row_index = 0, int column_index = 0) =>
//{
    _columns = get_nr_of_columns(m)
    val     = array.get(m, 2 + int((row_index * _columns) + column_index))
    val
//}

// @function Cuts off the metadata header and returns the array body, WITHOUT THE DIMENSIONAL METADATA!!
// do_slice This variable should be set as: - 'false' to only make a copy, changes to the new array copy will NOT ALTER the ORIGINAL - 'true' to make a slice, changes to the new array slice WILL(!) ALTER the ORIGINAL
export copy_slice_body(float[] m, bool do_slice = false) =>
//{  'do_slice' variable should be set as:
//      - 'false' to only make a copy,
//         changes to the new array copy will NOT ALTER the ORIGINAL
//      - 'true' to make a slice
//         changes to the new array slice WILL(!) ALTER the ORIGINAL
    _matrix_slice = array.slice(m, 2, array.size(m))
    _matrix_body  = do_slice ? _matrix_slice : array.copy(_matrix_slice)
    _matrix_body
//}

// @function Gets /retrieve the values from a ROW/RECORD from a certain row/lookback period, the values are returned as an array
export get_record(float[] m, int row_index = 0) =>
//{
    nr_of_columns = get_nr_of_columns(m)
    _start_index   = int(2 + row_index * nr_of_columns)
    _row_slice = array.slice(m, _start_index , _start_index + nr_of_columns)
    _record    = array.copy(_row_slice)
    _record
//}
//}

/// 1D - 2D COORDINATE CONVERSION FUNCTIONS (handy with for loop indexes) ///
//{
// @function Gets the row nr. in a 2D matrix from 1D index (index for rows and columns both starts at 0 !!)
export get_row_index(float[] m, int array_index = 0) =>
//{
    _columns   = get_nr_of_columns(m)
    row_index = int(math.floor(array_index / _columns))
    row_index
//}

// @function Gets the column nr. in a 2D matrix from 1D index (index for rows and columns both starts at 0 !!)
export get_column_index(float[] m, int array_index = 0) =>
//{
    _columns      = get_nr_of_columns(m)
    column_index = int(array_index % _columns)
    column_index
//}

// @function Gets a tuple with the (row, column) coordinates in 2D matrix from 1D index (index starts at 0 and does not include the header!!)
export get_row_column_index(float[] m, int array_index = 0) =>
//{
    _columns = get_nr_of_columns(m)
    _row     = int(math.floor(array_index / _columns))
    _column  = int(      array_index % _columns)
    [_row, _column]
//}

// @function Gets the 1D index from (row, column) coordinates in 2D matrix (index for row and column both starts at 0 !! Index starts at 0 and does not include the header!!)
export get_array_index(float[] m, int row_index = 0, int column_index = 0) =>
//{
    _columns = get_nr_of_columns(m)
    array_index = int(row_index * _columns + column_index)
    array_index
//}
//}

/// MANIPULATION FUNCTIONS ///
//{
// @function Removes one or more rows/records from a 2D matrix (if from_row = to_row, only this row is removed)
export remove_rows(float[] m, int from_row = 0, int to_row = 0) =>
//{
    [_rows, _columns] = get_size(m)
    if not(from_row == 0 and to_row == _rows - 1) and not(to_row < from_row)
        _start_remove_index = 2 + from_row * _columns
        for i = 1 to (math.min(_rows - 1, to_row) - from_row + 1) * _columns
            array.remove(m, math.max(_start_remove_index, 2))
        array.set(m, 0, _rows - (math.min(_rows - 1, to_row) - from_row) - 1)
    m
//}

// @function Remove one or more columns from a 2D matrix (if from_column = to_column, only this column is removed)
export remove_columns(float[] m, int from_column = 0, int to_column = 0) =>
//{
    [_rows, _columns] = get_size(m)
    if not(from_column == 0 and to_column == _columns - 1) and not(to_column < from_column)
        for _row_nr = (_rows - 1)  to 0
            _start_remove_index = 2 + _row_nr * _columns + from_column
            for i = from_column to to_column
                array.remove(m, math.max(_start_remove_index, 2))
        array.set(m, 1, _columns - (math.min(_columns, to_column) - from_column) - 1)
    m
//}

// @function Insert an array of rows/records at a certain row number in a 2D matrix
export insert_array_of_rows(float[] m, float[] insert_array, int from_row = 0) =>
//{
    [_rows, _columns] = get_size(m)
    [_rows_insert_array, _columns_insert_array] = get_size(insert_array)
    _size_insert_array = _columns_insert_array * _rows_insert_array
    if _columns == _columns_insert_array
        _insert_array_body  = copy_slice_body(insert_array, false)
        _start_insert_index = 2 + math.min(_rows, from_row) * _columns
        for index = (_size_insert_array - 1) to 0
            array.insert(m, _start_insert_index, array.get(_insert_array_body, index))
        array.set(m, 0, _rows + _rows_insert_array)
    m
//}

// @function ADDS a ROW/RECORD on the TOP of a sheet, shift the whole list one down and gives the option to REMOVE the OLDEST row/record. (2D version of "unshift" + "pop" but with a whole row at once)
export add_row(float[] m, float[] insert_array, bool remove_oldest = true) =>
//{
    insert_array_of_rows(m, insert_array, 0)
    [_rows, _columns] = get_size(m)
    if remove_oldest
        remove_rows(m, _rows - 1, _rows - 1)
//}

// @function Insert an array of columns at a certain column number in a 2D matrix
export insert_array_of_columns(float[] m, float[] insert_array, int from_column = 0) =>
//{
    [_rows, _columns] = get_size(m)
    [_rows_insert_array, _columns_insert_array] = get_size(insert_array)
    _size_insert_array = _columns_insert_array * _rows_insert_array
    if _rows == _rows_insert_array
        for _row_nr = (_rows - 1)  to 0
            _start_insert_index = 2 + _row_nr * _columns + math.min(_columns - 1, from_column)
            for index = 0 to (_columns_insert_array - 1)
                array.insert(m,
                 math.max(_start_insert_index, 2),
                  array.get(insert_array, int(2 + (_size_insert_array - 1) - index - ((_rows_insert_array - 1) - _row_nr) * _columns_insert_array)))
        array.set(m, 1, _columns + _columns_insert_array)
    m
//}

// @function Appends/adds an array of rows/records to the bottom of a 2D matrix
export append_array_of_rows(float[] m, float[] append_array) =>
//{
    [_rows, _columns] = get_size(m)
    [_rows_append_array, _columns_append_array] = get_size(append_array)
    if _columns == _columns_append_array
        _append_array_body = copy_slice_body(append_array, false)
        array.concat(m, _append_array_body)
        array.set(m, 0, _rows + _rows_append_array)
    m
//}

// @function Appends/adds an array of columns to the right side of a 2D matrix
export append_array_of_columns(float[] m, float[] append_array) =>
//{
    [_rows, _columns] = get_size(m)
    [_rows_append_array, _columns_append_array] = get_size(append_array)
    _size_append_array = _columns_append_array * _rows_append_array
    if _rows == _rows_append_array
        for _row_nr = (_rows - 1)  to 0
            _start_append_index = 2 + _row_nr * _columns + _columns
            for index = 0 to (_columns_append_array - 1)
                array.insert(m,
                 math.max(_start_append_index, 2),
                  array.get(append_array, int(2 + (_size_append_array - 1) - index - ((_rows_append_array - 1) - _row_nr) * _columns_append_array)))
        array.set(m, 1, _columns + _columns_append_array)
    m
//}

// @function Removes / pops and returns the last row/record from a 2D matrix.
export pop_row(float[] m) =>
//{
    [_rows, _columns] = get_size(m)
    _return_array = array.new_float()
    _start_remove_index = 2 + (_rows - 1) * _columns
    for i = 1 to _columns
        array.push(_return_array, array.get(m, math.max(_start_remove_index, 2)))
        array.remove(m, math.max(_start_remove_index, 2))
    array.set(m, 0, _rows - 1)
    _return_array
//}

// @function Removes / pops and returns the last (most right) column from a 2D matrix.
export pop_column(float[] m) =>
//{
    [_rows, _columns] = get_size(m)
    _return_array = array.new_float()
    for _row_nr = (_rows - 1)  to 0
        _start_remove_index = 2 + _row_nr * _columns + (_columns - 1)
        array.insert(_return_array, 0, array.get(m, math.max(_start_remove_index, 2)))
        array.remove(m, math.max(_start_remove_index, 2))
    array.set(m, 1, _columns - 1)
    _return_array
//}
//}

/// CALCULATION FUNCTIONS ///
//{
export replace(float[] m, float[] add_m) =>
//{
    [    _rows,     _columns] = get_size(m)
    [_add_rows, _add_columns] = get_size(add_m)
    if _rows == _add_rows and _columns == _add_columns
        for i = 0  to _rows - 1
            for j = 0  to _columns - 1
                add_value = get(add_m, i, j)
                set(m, i, j, add_value)
    m
//}

export abs(float[] m) =>
//{
    [    _rows,     _columns] = get_size(m)
    _abs_m = array.copy(m)
    for i = 0  to _rows - 1
        for j = 0  to _columns - 1
            _value = get(_abs_m, i, j)
            set(_abs_m, i, j, math.abs(_value))
    _abs_m
//}

// @function Returns a new matrix with the same value added to all the elements of the source matrix.
export add_value(float[] m, float add_val = 0) =>
//{
    [_rows, _columns] = get_size(m)
    _return_array = init(_rows, _columns, 0)
    for i = 0  to _rows - 1
        for j = 0  to _columns - 1
            _value = get(m, i, j)
            set(_return_array, i, j, _value + add_val)
    _return_array
//}

// @function Returns a new matrix with the of the elements of one 2D matrix added to every corresponding element of a source 2D matrix.
export addition(float[] m, float[] add_m) =>
//{
    [    _rows,     _columns] = get_size(m)
    [_add_rows, _add_columns] = get_size(add_m)
    _return_array = init(_rows, _columns, 0)
    if _rows == _add_rows and _columns == _add_columns
        for i = 0  to _rows - 1
            for j = 0  to _columns - 1
                _value      = get(    m, i, j)
                add_val   = get(add_m, i, j)
                set(_return_array, i, j, _value + add_val)
    _return_array
//}

// @function Returns a new matrix with the same value subtracted from every element of a 2D matrix 
export subtract_value(float[] m, float subtract_val = 0) =>
//{
    [_rows, _columns] = get_size(m)
    _return_array = init(_rows, _columns, 0)
    for i = 0  to _rows - 1
        for j = 0  to _columns - 1
            _value = get(m, i, j)
            set(_return_array, i, j, _value - subtract_val)
    _return_array
//}

// @function Returns a new matrix with the values of the elements of one 2D matrix subtracted from every corresponding element of a source 2D matrix.
export subtraction(float[] m, float[] subtract_m) =>
//{
    [         _rows,          _columns] = get_size(m)
    [_subtract_rows, _subtract_columns] = get_size(subtract_m)
    _return_array = init(_rows, _columns, 0)
    if _rows == _subtract_rows and _columns == _subtract_columns
        for i = 0  to _rows - 1
            for j = 0  to _columns - 1
                _value          = get(    m, i, j)
                subtract_val = get(subtract_m, i, j)
                set(_return_array, i, j, _value - subtract_val)
    _return_array
//}

// @function Returns a new matrix with all the elements of the source matrix scaled/multiplied by a scalar value.
export scalar_multipy(float[] m, float scalar = 1) =>
//{
    [_rows, _columns] = get_size(m)
    _return_array = init(_rows, _columns, 0)
    for i = 0  to _rows - 1
        for j = 0  to _columns - 1
            _value = get(m, i, j)
            set(_return_array, i, j, _value * scalar)
    _return_array
//}

// @function Returns a new matrix with the elements of the source matrix transposed.
export transpose(float[] m) =>
//{
    [_rows, _columns] = get_size(m)
    _return_array = init(_columns, _rows, 0)
    for i = 0  to _rows - 1
        for j = 0  to _columns - 1
            _value = get(m, i, j)
            set(_return_array, j, i, _value)
    _return_array
//}

// @function Performs ELEMENT WISE MULTIPLICATION of 2D matrices, returns a new matrix c.
export multiply_elem(float[] a, float[] b) =>
//{
    [_rows_a, _columns_a] = get_size(a)
    [_rows_b, _columns_b] = get_size(b)
    _return_array = init(_rows_a, _columns_a, 0)
    if _rows_a == _rows_b and _columns_a == _columns_b
        for i = 0  to _rows_a - 1
            for j = 0  to _columns_a - 1
                _value_a = get( a, i, j)
                _value_b = get( b, i, j)
                set(_return_array, i, j, _value_a * + _value_b)
    _return_array
//}

// @function Performs DOT PROCUCT MULTIPLICATION of 2D matrices, returns a new matrix c.
export multiply(float[] a, float[] b) =>
//{
    // get matrix metadata //
    [_a_row, _a_col] = get_size(a), _a_size = _a_row * _a_col
    [_b_row, _b_col] = get_size(b), _b_size = _b_row * _b_col
    _c_row = _a_row, _c_col = _b_col        , _c_size = _a_row * _b_col
    _c   = init(_a_row, _b_col, 0)
    // error check //
    if _a_col != _b_row
        array.fill(_c, 0)
    // multiplication //
    else
        _a_row_nr = 0, _a_col_nr = 0
        _b_row_nr = 0, _b_col_nr = 0
        _c_row_nr = 0, _c_col_nr = 0
        for i = 0 to (_c_size -1)
            i_val = 0.0
            _c_row_nr := math.floor(i / _b_col)
            // row_nr := row_nr + ((i % _b_x) == 0 ? 1 : 0)
            _c_col_nr := i % _b_col
            for j = 0 to (_a_col - 1)
                _a_row_nr :=     _c_row_nr , _a_col_nr := j % _a_col 
                _b_row_nr := j % _a_col    , _b_col_nr := _c_col_nr
                i_val := i_val + get(a, _a_row_nr, _a_col_nr) * get(b, _b_row_nr, _b_col_nr)
            array.set(_c, 2 + i, i_val)
    // return //
    _c
//}

// Matrix Determinants //
//{
// @function Calculates the determinant of 2x2 matrices.
export determinant_2x2(float[] m) =>
//{
    [_rows, _columns] = get_size(m)
    var float _determinant = na
    // error check //
    if _rows == _columns and _rows == 2
        // matrix form:
        //      |a   b|
        // det: |     |  =  a * d - c * b
        //      |c   d|
        
        a = get(m, 0, 0), b = get(m, 0, 1)
        _c = get(m, 1, 0), _d = get(m, 1, 1)
        _determinant := a * _d - _c * b
    _determinant
    
// @function Calculates the determinant of 3x3 matrices.
export determinant_3x3(float[] m) =>
//{
    [_rows, _columns] = get_size(m)
    var float _determinant = na
    // error check //
    if _rows == _columns and _rows == 3
        // matrix form:
        //      |a  b  c|
        // det: |d  e  f|  =  a * e * i  +  b * f * g  +  c * d * h  -  g * e * c   - d * b * i  -  a * h * f
        //      |g  h  i| 
        _determinant := 0.0
        for i = 0 to _columns - 1
            _cofactor_matrix = array.copy(m)
            remove_rows(   _cofactor_matrix, 0, 0)
            remove_columns(_cofactor_matrix, i, i)
            _determinant := _determinant + math.pow(-1, i + 1) * get(m, 0, i) * determinant_2x2(_cofactor_matrix)
    _determinant
//}
//}

// @function Calculates the determinant of 4x4 matrices.
export determinant_4x4(float[] m) =>
//{
    [_rows, _columns] = get_size(m)
    var float _determinant = na
    // error check //
    if _rows == _columns and _rows == 4
        _determinant := 0.0
        for i = 0 to _columns - 1
            _cofactor_matrix = array.copy(m)
            remove_rows(   _cofactor_matrix, 0, 0)
            remove_columns(_cofactor_matrix, i, i)
            _determinant := _determinant + math.pow(-1, i + 1) * get(m, 0, i) * determinant_3x3(_cofactor_matrix)
    _determinant
//}
//}
//}

// @function displays a 2D matrix in a table layout.
export print(float[] m, int rows = na, int columns = na, string pos = "top_right", color bg_color = color.teal, color text_color = color.black, string str_format = "##,###.##", string text_size = size.normal, string title = "matrix") =>
//{
    [_rows, _columns] = get_size(m)
    if not na(rows)
        _rows       := rows
        _columns    := columns
    var matrix_print = table.new(pos, _columns + 4, _rows + 2, bgcolor = bg_color)
    table.cell(matrix_print, 0, 0, text = title, text_size = text_size)
    table.cell(matrix_print, _columns + 3, 1, " ", text_size = text_size)
    for i = 1 to _rows
        table.cell(matrix_print, 1, i, "[", text_size = text_size)
        table.cell(matrix_print, _columns + 2, i, "]", text_size = text_size)
        if i > 0 and i < _rows + 1 
            for j = 0 to _columns - 1
                table.cell(matrix_print, j + 2, i,
                           (not na(rows) ? str.tostring(array.get(m, (i - 1) * _columns + j), str_format) : str.tostring(get(m, i - 1, j), str_format)),
                           text_size = text_size)
                table.cell_set_text_color(matrix_print, j + 1, i + 1, text_color)
//}
//}
//}

/// test section ///
// set test matrices
//{
a = from_list(3, 4, array.from(
                       11, 12, 13, 14,
                       21, 22, 23, 24,
                       31, 32, 33, 34))

b = array.copy(a)

// array to test add row functions
insert_row = from_list( 1, 4, array.from(
                                 10, 20, 30, 40))

// array to test insert/append row functions
insert_row_array = from_list(2, 4, array.from(
                                      10, 20 ,30, 40,
                                      50, 60, 70, 80))

// array to test add column functions
insert_column = from_list(3, 1, array.from(
                                   10,
                                   20,
                                   30,
                                   40))

// array to test insert/append column functions
insert_column_array = from_list(3, 2, array.from(
                                         10, 20,
                                         30, 40,
                                         50, 60))


// array to test element wise multiplication function
elem_multiplication_array = from_list(3, 4, array.from(
                                      10, 20 ,30, 40,
                                      20, 30, 40, 50,
                                      30, 40, 50, 60))
                                      
// array to test multiplication function
multiplication_array = from_list(4, 2, array.from(
                                          2, 3,
                                          2, 3,
                                          2, 3,
                                          2, 3))


// array to test determinant function
determinant_array = from_list(4, 4, array.from(
                                       4, 3, 2, 2,
                                       0, 1,-3, 3,
                                       0,-1, 3, 3,
                                       0, 3, 1, 1))
//}

// parameters to check behavior of functions
//{
x = input.int(0, title="from")
y = input.int(0, title="to (to chose only 1 row or column, set the same value for 'from' and 'to')")
c_type = input.string("none", title = "result matrix", options = ["none", "add rows to top and shift list", "get row/record from list", "remove array of rows", "remove array of columns", "insert array of rows",
                                                           "insert array of columns", "append array of rows", "append array of columns", "pop last row" , "pop last column", "element multiplication", "matrix multiplication",
                                                           "matrix transposition", "matrix determinant"])
//}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// test function calls ///                                                                                                   //
///////////////////////////
//                                                                                                                            //
// almost all functions can be called with or without a return array //                                                       //
// for example, both notations below can be used:                                                                             //
//                                                                                                                            //
//              c = append_array_of_rows(a, append_array) -> returns the appended matrix as array "c"                //
//                                                                                                                            //
//              append_array_of_rows(a, append_array)     -> just runs the function without creating a new array "c" //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

manipulation_array = 
 c_type == "add rows to top and shift list" ? insert_row :
 c_type == "remove array of rows"           ? insert_row_array :
 c_type == "remove array of columns"        ? insert_column_array :
 c_type == "insert array of rows"           ? insert_row_array :
 c_type == "insert array of columns"        ? insert_column_array :
 c_type == "append array of rows"           ? insert_row_array :
 c_type == "append array of columns"        ? insert_column_array :
 c_type == "element multiplication"         ? elem_multiplication_array :
 c_type == "matrix multiplication"          ? multiplication_array :
  insert_row

c = 
 c_type == "add rows to top and shift list" ? add_row(a, manipulation_array) :
 c_type == "remove array of rows"           ? remove_rows(a, x, y) :
 c_type == "remove array of columns"        ? remove_columns(a, x, y) :
 c_type == "insert array of rows"           ? insert_array_of_rows(a, manipulation_array, x) :
 c_type == "insert array of columns"        ? insert_array_of_columns(a, manipulation_array, x) :
 c_type == "append array of rows"           ? append_array_of_rows(a, manipulation_array) :
 c_type == "append array of columns"        ? append_array_of_columns(a, manipulation_array) :
 c_type == "element multiplication"         ? multiply_elem(a, manipulation_array) :
 c_type == "matrix multiplication"          ? multiply(a, manipulation_array) :
 c_type == "matrix transposition"           ? transpose(a) :
 c_type == "matrix determinant"             ? determinant_array :
 array.copy(a)

e =
 c_type == "get row/record from list" ? get_record(a, x) :
 c_type == "pop last row"             ? pop_row(a) :
 c_type == "pop last column"          ? pop_column(a) :
  array.copy(a)

// plot matrices
//{
// copy of original matrix before "manipulation"
if c_type != "matrix determinant"
    if barstate.isfirst
        print(b, pos = "middle_left", bg_color = color.teal, text_color = color.black, str_format = "#,###.##", text_size = size.normal, title = "original")

// "manipulation" matrix
if c_type != "remove array of rows" and c_type != "remove array of columns" and c_type != "get row/record from list"
 and c_type != "pop last row" and c_type != "pop last column" and c_type != "matrix transposition" and c_type != "matrix determinant" or c_type == "add rows to top and shift list"
    if barstate.isfirst
        print(manipulation_array, pos = "top_center", bg_color = color.teal, text_color = color.black, str_format = "#,###.##", text_size = size.normal, title = "work array")

// original matrix
if c_type != "matrix multiplication" and c_type != "element multiplication" and c_type != "matrix determinant" and c_type != "matrix transposition"
    if barstate.isfirst
        print(a, pos = "bottom_left", bg_color = color.teal, text_color = color.black, str_format = "#,###.##", text_size = size.normal, title = "modified original")

// function title
from    = " (from = " + str.tostring(x)
from_to = from + " -> to = " +  str.tostring(y) + " )"
title_txt = c_type +
 (c_type == "remove array of rows" or c_type == "remove array of columns" ? from_to :
  c_type == "insert array of rows" or c_type == "insert array of columns" or  c_type == "get row/record from list" ? from + " )" :
  c_type == "matrix determinant" ? " det = " + str.tostring(determinant_4x4(determinant_array)) : "")
if barstate.isfirst
    title_tab = table.new("bottom_center", 1, 1, bgcolor = color.orange)
    table.cell(title_tab, 0, 0, text = title_txt, text_color = color.black, text_size = size.large)

// copy of resulting matrix
if c_type != "get row/record from list" and c_type != "pop last row" and c_type != "pop last column" or c_type == "add rows to top and shift list"
    if barstate.isfirst
        print(c, pos = "middle_center", bg_color = color.teal, text_color = color.black, str_format = "#,###.##", text_size = size.normal, title = c_type == "matrix determinant" ? "original" : "result array")

// copy of resulting matrix
if c_type == "get row/record from list" or c_type == "pop last row" or c_type == "pop last column" and c_type != "matrix determinant"
    if barstate.isfirst
        result = array.copy(e)
        array.unshift(result, c_type == "pop last row" or c_type == "get row/record from list"? array.size(e) : 1)
        array.unshift(result, c_type == "pop last row" or c_type == "get row/record from list"? 1 : array.size(e))
        print(result, pos ="middle_center", bg_color = color.teal, text_color = color.black, str_format = "#,###.##", text_size = size.normal, title = "result array")
//}