import { createObjectPropListener, getProperty, isObject, objectEach, setProperty } from '../helpers/object.mjs';
import { countFirstRowKeys } from '../helpers/data.mjs';
import { arrayEach } from '../helpers/array.mjs';
import { rangeEach } from '../helpers/number.mjs';
import { isFunction } from '../helpers/function.mjs';
/**
 * @class DataSource
 * @private
 */ class DataSource {
    /**
   * Run the `modifyRowData` hook and return either the modified or the source data for the provided row.
   *
   * @private
   * @param {number} rowIndex Row index.
   * @returns {Array|object} Source or modified row of data.
   */ modifyRowData(rowIndex) {
        let modifyRowData;
        if (this.hot.hasHook('modifyRowData')) {
            modifyRowData = this.hot.runHooks('modifyRowData', rowIndex);
        }
        return modifyRowData !== undefined && !Number.isInteger(modifyRowData) ? modifyRowData : this.data[rowIndex];
    }
    /**
   * Get all data.
   *
   * @param {boolean} [toArray=false] If `true` return source data as an array of arrays even when source data was provided
   *                                  in another format.
   * @returns {Array}
   */ getData(toArray = false) {
        if (!this.data || this.data.length === 0) {
            return this.data;
        }
        return this.getByRange(null, null, toArray);
    }
    /**
   * Set new data source.
   *
   * @param {Array} data The new data.
   */ setData(data) {
        this.data = data;
    }
    /**
   * Returns array of column values from the data source. `column` is the index of the row in the data source.
   *
   * @param {number} column Visual column index.
   * @returns {Array}
   */ getAtColumn(column) {
        const result = [];
        arrayEach(this.data, (row, rowIndex)=>{
            const value = this.getAtCell(rowIndex, column);
            result.push(value);
        });
        return result;
    }
    /**
   * Returns a single row of the data or a subset of its columns. If a column range or `toArray` arguments are provided, it
   * operates only on the columns declared by the `columns` setting or the data schema.
   *
   * @param {number} row Physical row index.
   * @param {number} [startColumn] Starting index for the column range (optional).
   * @param {number} [endColumn] Ending index for the column range (optional).
   * @param {boolean} [toArray=false] `true` if the returned value should be forced to be presented as an array.
   * @returns {Array|object}
   */ getAtRow(row, startColumn, endColumn, toArray = false) {
        const getAllProps = startColumn === undefined && endColumn === undefined;
        const { dataDotNotation } = this.hot.getSettings();
        let dataRow = null;
        let newDataRow = null;
        dataRow = this.modifyRowData(row);
        if (Array.isArray(dataRow)) {
            newDataRow = [];
            if (getAllProps) {
                dataRow.forEach((cell, column)=>{
                    newDataRow[column] = this.getAtPhysicalCell(row, column, dataRow);
                });
            } else {
                // Only the columns from the provided range
                rangeEach(startColumn, endColumn, (column)=>{
                    newDataRow[column - startColumn] = this.getAtPhysicalCell(row, column, dataRow);
                });
            }
        } else if (isObject(dataRow) || isFunction(dataRow)) {
            if (toArray) {
                newDataRow = [];
            } else {
                newDataRow = {};
            }
            if (!getAllProps || toArray) {
                const rangeStart = 0;
                const rangeEnd = this.countFirstRowKeys() - 1;
                rangeEach(rangeStart, rangeEnd, (column)=>{
                    const prop = this.colToProp(column);
                    if (column >= (startColumn || rangeStart) && column <= (endColumn || rangeEnd) && !Number.isInteger(prop)) {
                        const cellValue = this.getAtPhysicalCell(row, prop, dataRow);
                        if (toArray) {
                            newDataRow.push(cellValue);
                        } else if (dataDotNotation) {
                            setProperty(newDataRow, prop, cellValue);
                        } else {
                            newDataRow[prop] = cellValue;
                        }
                    }
                });
            } else {
                objectEach(dataRow, (value, prop)=>{
                    const cellValue = this.getAtPhysicalCell(row, prop, dataRow);
                    if (dataDotNotation) {
                        setProperty(newDataRow, prop, cellValue);
                    } else {
                        newDataRow[prop] = cellValue;
                    }
                });
            }
        }
        return newDataRow;
    }
    /**
   * Set the provided value in the source data set at the provided coordinates.
   *
   * @param {number} row Physical row index.
   * @param {number|string} column Property name / physical column index.
   * @param {*} value The value to be set at the provided coordinates.
   */ setAtCell(row, column, value) {
        if (row >= this.countRows() || column >= this.countFirstRowKeys()) {
            // Not enough rows and/or columns.
            return;
        }
        if (this.hot.hasHook('modifySourceData')) {
            const valueHolder = createObjectPropListener(value);
            this.hot.runHooks('modifySourceData', row, column, valueHolder, 'set');
            if (valueHolder.isTouched()) {
                value = valueHolder.value;
            }
        }
        if ([
            '__proto__',
            'constructor',
            'prototype'
        ].includes(row)) {
            // prevent prototype pollution
            return;
        }
        const dataRow = this.modifyRowData(row);
        if (!Number.isInteger(column)) {
            // column argument is the prop name
            setProperty(dataRow, column, value);
        } else {
            dataRow[column] = value;
        }
    }
    /**
   * Get data from the source data set using the physical indexes.
   *
   * @private
   * @param {number} row Physical row index.
   * @param {string|number|Function} column Physical column index / property / function.
   * @param {Array|object} dataRow A representation of a data row.
   * @returns {*} Value at the provided coordinates.
   */ getAtPhysicalCell(row, column, dataRow) {
        let result = null;
        if (dataRow) {
            if (typeof column === 'string') {
                const { dataDotNotation } = this.hot.getSettings();
                result = dataDotNotation ? getProperty(dataRow, column) : dataRow[column];
            } else if (typeof column === 'function') {
                result = column(dataRow);
            } else {
                result = dataRow[column];
            }
        }
        if (this.hot.hasHook('modifySourceData')) {
            const valueHolder = createObjectPropListener(result);
            this.hot.runHooks('modifySourceData', row, column, valueHolder, 'get');
            if (valueHolder.isTouched()) {
                result = valueHolder.value;
            }
        }
        return result;
    }
    /**
   * Returns a single value from the data.
   *
   * @param {number} row Physical row index.
   * @param {number} columnOrProp Visual column index or property.
   * @returns {*}
   */ getAtCell(row, columnOrProp) {
        const dataRow = this.modifyRowData(row);
        return this.getAtPhysicalCell(row, this.colToProp(columnOrProp), dataRow);
    }
    /**
   * Returns source data by passed range.
   *
   * @param {object} [start] Object with physical `row` and `col` keys (or visual column index, if data type is an array of objects).
   * @param {object} [end] Object with physical `row` and `col` keys (or visual column index, if data type is an array of objects).
   * @param {boolean} [toArray=false] If `true` return source data as an array of arrays even when source data was provided
   *                                  in another format.
   * @returns {Array}
   */ getByRange(start = null, end = null, toArray = false) {
        let getAllProps = false;
        let startRow = null;
        let startCol = null;
        let endRow = null;
        let endCol = null;
        if (start === null || end === null) {
            getAllProps = true;
            startRow = 0;
            endRow = this.countRows() - 1;
        } else {
            startRow = Math.min(start.row, end.row);
            startCol = Math.min(start.col, end.col);
            endRow = Math.max(start.row, end.row);
            endCol = Math.max(start.col, end.col);
        }
        const result = [];
        rangeEach(startRow, endRow, (currentRow)=>{
            result.push(getAllProps ? this.getAtRow(currentRow, undefined, undefined, toArray) : this.getAtRow(currentRow, startCol, endCol, toArray));
        });
        return result;
    }
    /**
   * Returns single value from the data array (intended for clipboard copy to an external application).
   *
   * @param {number} row Visual row index.
   * @param {number} prop The column property.
   * @since 16.1.0
   * @returns {string}
   */ getCopyable(row, prop) {
        const visualColumn = this.propToCol(prop);
        if (this.hot.getCellMeta(row, visualColumn).copyable) {
            return this.getAtCell(this.hot.toPhysicalRow(row), visualColumn);
        }
        return '';
    }
    /**
   * Count number of rows.
   *
   * @returns {number}
   */ countRows() {
        if (this.hot.hasHook('modifySourceLength')) {
            const modifiedSourceLength = this.hot.runHooks('modifySourceLength');
            if (Number.isInteger(modifiedSourceLength)) {
                return modifiedSourceLength;
            }
        }
        return this.data.length;
    }
    /**
   * Count number of columns.
   *
   * @returns {number}
   */ countFirstRowKeys() {
        return countFirstRowKeys(this.data);
    }
    /**
   * Destroy instance.
   */ destroy() {
        this.data = null;
        this.hot = null;
    }
    constructor(hotInstance, dataSource = []){
        /**
   * Type of data source.
   *
   * @type {string}
   * @default 'array'
   */ this.dataType = 'array';
        this.colToProp = ()=>{};
        this.propToCol = ()=>{};
        this.hot = hotInstance;
        this.data = dataSource;
    }
}
export default DataSource;
