"use strict";

exports.__esModule = true;
require("core-js/modules/es.error.cause.js");
require("core-js/modules/es.array.push.js");
var _array = require("../../helpers/array");
var _console = require("../../helpers/console");
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
/**
 * Class used to make all endpoint-related operations.
 *
 * @private
 * @class Endpoints
 */
class Endpoints {
  constructor(plugin, settings) {
    /**
     * The main plugin instance.
     */
    _defineProperty(this, "plugin", void 0);
    /**
     * Handsontable instance.
     *
     * @type {object}
     */
    _defineProperty(this, "hot", void 0);
    /**
     * Array of declared plugin endpoints (calculation destination points).
     *
     * @type {Array}
     * @default {Array} Empty array.
     */
    _defineProperty(this, "endpoints", []);
    /**
     * The plugin settings, taken from Handsontable configuration.
     *
     * @type {object|Function}
     * @default null
     */
    _defineProperty(this, "settings", void 0);
    /**
     * Settings type. Can be either 'array' or 'function'.
     *
     * @type {string}
     * @default {'array'}
     */
    _defineProperty(this, "settingsType", 'array');
    /**
     * The current endpoint (calculation destination point) in question.
     *
     * @type {object}
     * @default null
     */
    _defineProperty(this, "currentEndpoint", null);
    /**
     * Array containing a list of changes to be applied.
     *
     * @private
     * @type {Array}
     * @default {[]}
     */
    _defineProperty(this, "cellsToSetCache", []);
    this.plugin = plugin;
    this.hot = this.plugin.hot;
    this.settings = settings;
  }

  /**
   * Get a single endpoint object.
   *
   * @param {number} index Index of the endpoint.
   * @returns {object}
   */
  getEndpoint(index) {
    if (this.settingsType === 'function') {
      return this.fillMissingEndpointData(this.settings)[index];
    }
    return this.endpoints[index];
  }

  /**
   * Get an array with all the endpoints.
   *
   * @returns {Array}
   */
  getAllEndpoints() {
    if (this.settingsType === 'function') {
      return this.fillMissingEndpointData(this.settings);
    }
    return this.endpoints;
  }

  /**
   * Used to fill the blanks in the endpoint data provided by a settings function.
   *
   * @private
   * @param {Function} func Function provided in the HOT settings.
   * @returns {Array} An array of endpoints.
   */
  fillMissingEndpointData(func) {
    return this.parseSettings(func.call(this));
  }

  /**
   * Parse plugin's settings.
   *
   * @param {Array} settings The settings array.
   * @returns {object[]}
   */
  parseSettings(settings) {
    const endpointsArray = [];
    let settingsArray = settings;
    if (!settingsArray && typeof this.settings === 'function') {
      this.settingsType = 'function';
      return;
    }
    if (!settingsArray) {
      settingsArray = this.settings;
    }
    (0, _array.arrayEach)(settingsArray, val => {
      const newEndpoint = {};
      this.assignSetting(val, newEndpoint, 'ranges', [[0, this.hot.countRows() - 1]]);
      this.assignSetting(val, newEndpoint, 'reversedRowCoords', false);
      this.assignSetting(val, newEndpoint, 'destinationRow', new Error(`
        You must provide a destination row for the Column Summary plugin in order to work properly!
      `));
      this.assignSetting(val, newEndpoint, 'destinationColumn', new Error(`
        You must provide a destination column for the Column Summary plugin in order to work properly!
      `));
      this.assignSetting(val, newEndpoint, 'sourceColumn', val.destinationColumn);
      this.assignSetting(val, newEndpoint, 'type', 'sum');
      this.assignSetting(val, newEndpoint, 'forceNumeric', false);
      this.assignSetting(val, newEndpoint, 'suppressDataTypeErrors', true);
      this.assignSetting(val, newEndpoint, 'suppressDataTypeErrors', true);
      this.assignSetting(val, newEndpoint, 'customFunction', null);
      this.assignSetting(val, newEndpoint, 'readOnly', true);
      this.assignSetting(val, newEndpoint, 'roundFloat', false);
      endpointsArray.push(newEndpoint);
    });
    return endpointsArray;
  }

  /**
   * Setter for the internal setting objects.
   *
   * @param {object} settings Object with the settings.
   * @param {object} endpoint Contains information about the endpoint for the the calculation.
   * @param {string} name Settings name.
   * @param {object} defaultValue Default value for the settings.
   */
  assignSetting(settings, endpoint, name, defaultValue) {
    if (name === 'ranges' && settings[name] === undefined) {
      endpoint[name] = defaultValue;
      return;
    } else if (name === 'ranges' && settings[name].length === 0) {
      return;
    }
    if (settings[name] === undefined) {
      if (defaultValue instanceof Error) {
        throw defaultValue;
      }
      endpoint[name] = defaultValue;
    } else {
      /* eslint-disable no-lonely-if */
      if (name === 'destinationRow' && endpoint.reversedRowCoords) {
        endpoint[name] = this.hot.countRows() - settings[name] - 1;
      } else {
        endpoint[name] = settings[name];
      }
    }
  }

  /**
   * Resets the endpoint setup before the structure alteration (like inserting or removing rows/columns). Used for settings provided as a function.
   *
   * @private
   * @param {string} action Type of the action performed.
   * @param {number} index Row/column index.
   * @param {number} number Number of rows/columns added/removed.
   */
  resetSetupBeforeStructureAlteration(action, index, number) {
    if (this.settingsType !== 'function') {
      return;
    }
    const type = action.indexOf('row') > -1 ? 'row' : 'col';
    const endpoints = this.getAllEndpoints();
    (0, _array.arrayEach)(endpoints, val => {
      if (type === 'row' && val.destinationRow >= index) {
        if (action === 'insert_row') {
          val.alterRowOffset = number;
        } else if (action === 'remove_row') {
          val.alterRowOffset = -1 * number;
        }
      }
      if (type === 'col' && val.destinationColumn >= index) {
        if (action === 'insert_col') {
          val.alterColumnOffset = number;
        } else if (action === 'remove_col') {
          val.alterColumnOffset = -1 * number;
        }
      }
    });
    this.resetAllEndpoints(endpoints, false);
  }

  /**
   * AfterCreateRow/afterCreateRow/afterRemoveRow/afterRemoveCol hook callback. Reset and reenables the summary functionality
   * after changing the table structure.
   *
   * @private
   * @param {string} action Type of the action performed.
   * @param {number} index Row/column index.
   * @param {number} number Number of rows/columns added/removed.
   * @param {Array} [logicRows] Array of the logical indexes.
   * @param {string} [source] Source of change.
   * @param {boolean} [forceRefresh] `true` of the endpoints should refresh after completing the function.
   */
  resetSetupAfterStructureAlteration(action, index, number, logicRows, source) {
    let forceRefresh = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : true;
    if (this.settingsType === 'function') {
      // We need to run it on a next avaiable hook, because the TrimRows' `afterCreateRow` hook triggers after this one,
      // and it needs to be run to properly calculate the endpoint value.
      const beforeViewRenderCallback = () => {
        this.hot.removeHook('beforeViewRender', beforeViewRenderCallback);
        return this.refreshAllEndpoints();
      };
      this.hot.addHookOnce('beforeViewRender', beforeViewRenderCallback);
      return;
    }
    const type = action.indexOf('row') > -1 ? 'row' : 'col';
    const multiplier = action.indexOf('remove') > -1 ? -1 : 1;
    const endpoints = this.getAllEndpoints();
    const rowMoving = action.indexOf('move_row') === 0;
    const placeOfAlteration = index;
    (0, _array.arrayEach)(endpoints, val => {
      if (type === 'row' && val.destinationRow >= placeOfAlteration) {
        val.alterRowOffset = multiplier * number;
      }
      if (type === 'col' && val.destinationColumn >= placeOfAlteration) {
        val.alterColumnOffset = multiplier * number;
      }
    });
    this.resetAllEndpoints(endpoints, !rowMoving);
    if (rowMoving) {
      (0, _array.arrayEach)(endpoints, endpoint => {
        this.extendEndpointRanges(endpoint, placeOfAlteration, logicRows[0], logicRows.length);
        this.recreatePhysicalRanges(endpoint);
        this.clearOffsetInformation(endpoint);
      });
    } else {
      (0, _array.arrayEach)(endpoints, endpoint => {
        this.shiftEndpointCoordinates(endpoint, placeOfAlteration);
      });
    }
    if (forceRefresh) {
      this.refreshAllEndpoints();
    }
  }

  /**
   * Clear the offset information from the endpoint object.
   *
   * @private
   * @param {object} endpoint And endpoint object.
   */
  clearOffsetInformation(endpoint) {
    endpoint.alterRowOffset = undefined;
    endpoint.alterColumnOffset = undefined;
  }

  /**
   * Extend the row ranges for the provided endpoint.
   *
   * @private
   * @param {object} endpoint The endpoint object.
   * @param {number} placeOfAlteration Index of the row where the alteration takes place.
   * @param {number} previousPosition Previous endpoint result position.
   * @param {number} offset Offset generated by the alteration.
   */
  extendEndpointRanges(endpoint, placeOfAlteration, previousPosition, offset) {
    (0, _array.arrayEach)(endpoint.ranges, range => {
      // is a range, not a single row
      if (range[1]) {
        if (placeOfAlteration >= range[0] && placeOfAlteration <= range[1]) {
          if (previousPosition > range[1]) {
            range[1] += offset;
          } else if (previousPosition < range[0]) {
            range[0] -= offset;
          }
        } else if (previousPosition >= range[0] && previousPosition <= range[1]) {
          range[1] -= offset;
          if (placeOfAlteration <= range[0]) {
            range[0] += 1;
            range[1] += 1;
          }
        }
      }
    });
  }

  /**
   * Recreate the physical ranges for the provided endpoint. Used (for example) when a row gets moved and extends an existing range.
   *
   * @private
   * @param {object} endpoint An endpoint object.
   */
  recreatePhysicalRanges(endpoint) {
    const ranges = endpoint.ranges;
    const newRanges = [];
    const allIndexes = [];
    (0, _array.arrayEach)(ranges, range => {
      const newRange = [];
      if (range[1]) {
        for (let i = range[0]; i <= range[1]; i++) {
          newRange.push(this.hot.toPhysicalRow(i));
        }
      } else {
        newRange.push(this.hot.toPhysicalRow(range[0]));
      }
      allIndexes.push(newRange);
    });
    (0, _array.arrayEach)(allIndexes, range => {
      let newRange = [];
      (0, _array.arrayEach)(range, (coord, index) => {
        if (index === 0) {
          newRange.push(coord);
        } else if (range[index] !== range[index - 1] + 1) {
          newRange.push(range[index - 1]);
          newRanges.push(newRange);
          newRange = [];
          newRange.push(coord);
        }
        if (index === range.length - 1) {
          newRange.push(coord);
          newRanges.push(newRange);
        }
      });
    });
    endpoint.ranges = newRanges;
  }

  /**
   * Shifts the endpoint coordinates by the defined offset.
   *
   * @private
   * @param {object} endpoint Endpoint object.
   * @param {number} offsetStartIndex Index of the performed change (if the change is located after the endpoint, nothing about the endpoint has to be changed.
   */
  shiftEndpointCoordinates(endpoint, offsetStartIndex) {
    if (endpoint.alterRowOffset && endpoint.alterRowOffset !== 0) {
      endpoint.destinationRow += endpoint.alterRowOffset || 0;
      (0, _array.arrayEach)(endpoint.ranges, element => {
        (0, _array.arrayEach)(element, (subElement, j) => {
          if (subElement >= offsetStartIndex) {
            element[j] += endpoint.alterRowOffset || 0;
          }
        });
      });
    } else if (endpoint.alterColumnOffset && endpoint.alterColumnOffset !== 0) {
      endpoint.destinationColumn += endpoint.alterColumnOffset || 0;
      endpoint.sourceColumn += endpoint.alterColumnOffset || 0;
    }
  }

  /**
   * Resets (removes) the endpoints from the table.
   *
   * @param {Array} [endpoints] Array containing the endpoints.
   * @param {boolean} [useOffset=true] Use the cell offset value.
   */
  resetAllEndpoints() {
    let endpoints = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.getAllEndpoints();
    let useOffset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
    const anyEndpointOutOfRange = endpoints.some(endpoint => {
      const alterRowOffset = endpoint.alterRowOffset || 0;
      const alterColOffset = endpoint.alterColumnOffset || 0;
      if (endpoint.destinationRow + alterRowOffset >= this.hot.countRows() || endpoint.destinationColumn + alterColOffset >= this.hot.countCols()) {
        return true;
      }
      return false;
    });
    if (anyEndpointOutOfRange) {
      return;
    }
    this.cellsToSetCache = [];
    (0, _array.arrayEach)(endpoints, endpoint => {
      this.resetEndpointValue(endpoint, useOffset);
    });
    this.hot.setDataAtCell(this.cellsToSetCache, 'ColumnSummary.reset');
    this.cellsToSetCache = [];
  }

  /**
   * Calculate and refresh all defined endpoints.
   */
  refreshAllEndpoints() {
    this.cellsToSetCache = [];
    (0, _array.arrayEach)(this.getAllEndpoints(), value => {
      this.currentEndpoint = value;
      this.plugin.calculate(value);
      this.setEndpointValue(value, 'init');
    });
    this.currentEndpoint = null;
    this.hot.setDataAtCell(this.cellsToSetCache, 'ColumnSummary.reset');
    this.cellsToSetCache = [];
  }

  /**
   * Calculate and refresh endpoints only in the changed columns.
   *
   * @param {Array} changes Array of changes from the `afterChange` hook.
   */
  refreshChangedEndpoints(changes) {
    const needToRefresh = [];
    this.cellsToSetCache = [];
    (0, _array.arrayEach)(changes, (value, key, changesObj) => {
      // if nothing changed, dont update anything
      if (`${value[2] || ''}` === `${value[3]}`) {
        return;
      }
      (0, _array.arrayEach)(this.getAllEndpoints(), (endpoint, j) => {
        if (this.hot.propToCol(changesObj[key][1]) === endpoint.sourceColumn && needToRefresh.indexOf(j) === -1) {
          needToRefresh.push(j);
        }
      });
    });
    (0, _array.arrayEach)(needToRefresh, value => {
      this.refreshEndpoint(this.getEndpoint(value));
    });
    this.hot.setDataAtCell(this.cellsToSetCache, 'ColumnSummary.reset');
    this.cellsToSetCache = [];
  }

  /**
   * Calculate and refresh a single endpoint.
   *
   * @param {object} endpoint Contains the endpoint information.
   */
  refreshEndpoint(endpoint) {
    this.currentEndpoint = endpoint;
    this.plugin.calculate(endpoint);
    this.setEndpointValue(endpoint);
    this.currentEndpoint = null;
  }

  /**
   * Reset the endpoint value.
   *
   * @param {object} endpoint Contains the endpoint information.
   * @param {boolean} [useOffset=true] Use the cell offset value.
   */
  resetEndpointValue(endpoint) {
    let useOffset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
    const alterRowOffset = endpoint.alterRowOffset || 0;
    const alterColOffset = endpoint.alterColumnOffset || 0;
    this.cellsToSetCache.push([this.hot.toVisualRow(endpoint.destinationRow + (useOffset ? alterRowOffset : 0)), this.hot.toVisualColumn(endpoint.destinationColumn + (useOffset ? alterColOffset : 0)), '']);
  }

  /**
   * Set the endpoint value.
   *
   * @param {object} endpoint Contains the endpoint information.
   * @param {string} [source] Source of the call information.
   * @param {boolean} [render=false] `true` if it needs to render the table afterwards.
   */
  setEndpointValue(endpoint, source) {
    let render = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
    const visualEndpointRowIndex = this.hot.toVisualRow(endpoint.destinationRow);
    if (endpoint.destinationRow >= this.hot.countRows() || endpoint.destinationColumn >= this.hot.countCols()) {
      this.throwOutOfBoundsWarning();
      return;
    }
    const destinationVisualRow = this.hot.toVisualRow(endpoint.destinationRow);
    if (destinationVisualRow !== null) {
      const cellMeta = this.hot.getCellMeta(destinationVisualRow, endpoint.destinationColumn);
      if (source === 'init' || cellMeta.readOnly !== endpoint.readOnly) {
        cellMeta.readOnly = endpoint.readOnly;
        cellMeta.className = 'columnSummaryResult';
      }
    }
    if (endpoint.roundFloat && !isNaN(endpoint.result)) {
      endpoint.result = endpoint.result.toFixed(endpoint.roundFloat);
    }
    if (render) {
      this.hot.setDataAtCell(visualEndpointRowIndex, endpoint.destinationColumn, endpoint.result, 'ColumnSummary.set');
    } else {
      this.cellsToSetCache.push([visualEndpointRowIndex, endpoint.destinationColumn, endpoint.result]);
    }
    endpoint.alterRowOffset = undefined;
    endpoint.alterColumnOffset = undefined;
  }

  /**
   * Throw an error for the calculation range being out of boundaries.
   *
   * @private
   */
  throwOutOfBoundsWarning() {
    (0, _console.warn)('One of the Column Summary plugins\' destination points you provided is beyond the table boundaries!');
  }
}
var _default = Endpoints;
exports.default = _default;