/*
  Licensed under the MIT license
  Copyright (c) 2014 handsontable
*/
(function(Handsontable) {
  'use strict';

  // [MODIFICATION] (sci 2588)
  var formulasResults = {};
  // _!_MODIFICATION
  function HandsontableFormula() {

    var isFormula = function(value) {
      if (value) {
        if (value[0] === '=') {
          return true;
        }
      }

      return false;
    };

    // [MODIFICATION] (sci 2588)
    var beforeRender = function (isForced) {
          formulasResults = {};
          var data = this.getData();
          for (var i = 0; i < data.length; ++i) {
              for (var j = 0; j < data[i].length; ++j) {
                  var value = data[i][j];
                  if (value && value[0] === '=') {
                      var cellId = this.plugin.utils.translateCellCoords({row: i, col: j});
                      this.plugin.matrix.removeItem(cellId);
                  }
              }
          }
    };
    // _!_MODIFICATION

    var formulaRenderer = function(instance, TD, row, col, prop, value, cellProperties) {
      if (instance.formulasEnabled && isFormula(value)) {
        // translate coordinates into cellId
        var cellId = instance.plugin.utils.translateCellCoords({
              row: row,
              col: col
            }),
            prevFormula = null,
            formula = null,
            needUpdate = false,
            error, result;

        if (!cellId) {
          return;
        }

        // get cell data
        var item = instance.plugin.matrix.getItem(cellId);

        if (item) {

          needUpdate = !! item.needUpdate;

          if (item.error) {
            prevFormula = item.formula;
            error = item.error;

            if (needUpdate) {
              error = null;
            }
          }
        }

        // check if typed formula or cell value should be recalculated
        if ((value && value[0] === '=') || needUpdate) {

          // [MODIFICATION] (sci 2588)
          if (formulasResults[cellId] === undefined) {
          // _!_MODIFICATION

            formula = value.substr(1).toUpperCase();

            if (!error || formula !== prevFormula) {

              var currentItem = item;

              if (!currentItem) {

                // define item to rulesJS matrix if not exists
                item = {
                  id: cellId,
                  formula: formula
                };

                // add item to matrix
                currentItem = instance.plugin.matrix.addItem(item);
              }

              // parse formula
              var newValue = instance.plugin.parse(formula, {
                row: row,
                col: col,
                id: cellId
              });

              // check if update needed
              needUpdate = (newValue.error === '#NEED_UPDATE');

              // update item value and error
              instance.plugin.matrix.updateItem(currentItem, {
                formula: formula,
                value: newValue.result,
                error: newValue.error,
                needUpdate: needUpdate
              });

              error = newValue.error;
              result = newValue.result;

              // update cell value in hot
              value = error || result;
            }
        // [MODIFICATION] (sci 2588)
        } else {
          var newValue = formulasResults[cellId];

          error = newValue.error;
          result = newValue.result;

          value = error || result;
        }
        // _!_MODIFICATION
      }
        if (error) {
          // clear cell value
          if (!value) {
            // reset error
            error = null;
          } else {
            // show error
            value = error;
          }
        }
      }

      // apply changes
      if (cellProperties.type === 'numeric') {
        numericCell.renderer.apply(this, [instance, TD, row, col, prop, value, cellProperties]);
      } else {
        textCell.renderer.apply(this, [instance, TD, row, col, prop, value, cellProperties]);
      }
    };

    var afterChange = function(changes, source) {
      var instance = this;

      if (!instance.formulasEnabled) {
        return;
      }

      if (source === 'edit' || source === 'undo' || source === 'autofill') {

        var rerender = false;

        changes.forEach(function(item) {

          var row = item[0],
              col = item[1],
              prevValue = item[2],
              value = item[3];

          var cellId = instance.plugin.utils.translateCellCoords({
            row: row,
            col: col
          });

          // if changed value, all references cells should be recalculated
          if (value[0] !== '=' || prevValue !== value) {
            instance.plugin.matrix.removeItem(cellId);

            // get referenced cells
            var deps = instance.plugin.matrix.getDependencies(cellId);

            // update cells
            deps.forEach(function(itemId) {
              instance.plugin.matrix.updateItem(itemId, {
                needUpdate: true
              });
            });

            rerender = true;
          }
        });

        if (rerender) {
          instance.render();
        }
      }
    };

    var beforeAutofillInsidePopulate = function(index, direction, data, deltas, iterators, selected) {
      var instance = this;

      var r = index.row,
          c = index.col,
          value = data[r][c],
          delta = 0,
          rlength = data.length, // rows
          clength = data ? data[0].length : 0; //cols

      if (value[0] === '=') { // formula

        if (['down', 'up'].indexOf(direction) !== -1) {
          delta = rlength * iterators.row;
        } else if (['right', 'left'].indexOf(direction) !== -1) {
          delta = clength * iterators.col;
        }

        return {
          value: instance.plugin.utils.updateFormula(value, direction, delta),
          iterators: iterators
        }

      } else { // other value

        // increment or decrement  values for more than 2 selected cells
        if (rlength >= 2 || clength >= 2) {

          var newValue = instance.plugin.helper.number(value),
              ii,
              start;

          if (instance.plugin.utils.isNumber(newValue)) {

            if (['down', 'up'].indexOf(direction) !== -1) {

              delta = deltas[0][c];

              if (direction === 'down') {
                newValue += (delta * rlength * iterators.row);
              } else {

                ii = (selected.row - r) % rlength;
                start = ii > 0 ? rlength - ii : 0;

                newValue = instance.plugin.helper.number(data[start][c]);

                newValue += (delta * rlength * iterators.row);

                // last element in array -> decrement iterator
                // iterator cannot be less than 1
                if (iterators.row > 1 && (start + 1) === rlength) {
                  iterators.row--;
                }
              }

            } else if (['right', 'left'].indexOf(direction) !== -1) {
              delta = deltas[r][0];

              if (direction === 'right') {
                newValue += (delta * clength * iterators.col);
              } else {

                ii = (selected.col - c) % clength;
                start = ii > 0 ? clength - ii : 0;

                newValue = instance.plugin.helper.number(data[r][start]);

                newValue += (delta * clength * (iterators.col || 1));

                // last element in array -> decrement iterator
                // iterator cannot be less than 1
                if (iterators.col > 1 && (start + 1) === clength) {
                  iterators.col--;
                }
              }
            }

            return {
              value: newValue,
              iterators: iterators
            }
          }
        }

      }

      return {
        value: value,
        iterators: iterators
      };
    };

    var afterCreateRow = function(row, amount) {

      var instance = this;

      var selectedRow = instance.plugin.utils.isArray(instance.getSelected()) ? instance.getSelected()[0] : undefined;

      if (instance.plugin.utils.isUndefined(selectedRow)) {
        return;
      }

      var direction = (selectedRow >= row) ? 'before' : 'after',
          items = instance.plugin.matrix.getRefItemsToRow(row),
          counter = 1,
          changes = [];

      items.forEach(function(id) {
        var item = instance.plugin.matrix.getItem(id),
            formula = instance.plugin.utils.changeFormula(item.formula, 1, {
              row: row
            }), // update formula if needed
            newId = id;

        if (formula !== item.formula) { // formula updated

          // change row index and get new coordinates
          if ((direction === 'before' && selectedRow <= item.row) || (direction === 'after' && selectedRow < item.row)) {
            newId = instance.plugin.utils.changeRowIndex(id, counter);
          }

          var cellCoords = instance.plugin.utils.cellCoords(newId);

          if (newId !== id) {
            // remove current item from matrix
            instance.plugin.matrix.removeItem(id);
          }

          // set updated formula in new cell
          changes.push([cellCoords.row, cellCoords.col, '=' + formula]);
        }
      });

      if (items) {
        instance.plugin.matrix.removeItemsBelowRow(row);
      }

      if (changes) {
        instance.setDataAtCell(changes);
      }
    };

    var afterCreateCol = function(col) {
      var instance = this;

      var selectedCol = instance.plugin.utils.isArray(instance.getSelected()) ? instance.getSelected()[1] : undefined;

      if (instance.plugin.utils.isUndefined(selectedCol)) {
        return;
      }

      var items = instance.plugin.matrix.getRefItemsToColumn(col),
          counter = 1,
          direction = (selectedCol >= col) ? 'before' : 'after',
          changes = [];

      items.forEach(function(id) {

        var item = instance.plugin.matrix.getItem(id),
            formula = instance.plugin.utils.changeFormula(item.formula, 1, {
              col: col
            }), // update formula if needed
            newId = id;

        if (formula !== item.formula) { // formula updated

          // change col index and get new coordinates
          if ((direction === 'before' && selectedCol <= item.col) || (direction === 'after' && selectedCol < item.col)) {
            newId = instance.plugin.utils.changeColIndex(id, counter);
          }

          var cellCoords = instance.plugin.utils.cellCoords(newId);

          if (newId !== id) {
            // remove current item from matrix if id changed
            instance.plugin.matrix.removeItem(id);
          }

          // set updated formula in new cell
          changes.push([cellCoords.row, cellCoords.col, '=' + formula]);
        }
      });

      if (items) {
        instance.plugin.matrix.removeItemsBelowCol(col);
      }

      if (changes) {
        instance.setDataAtCell(changes);
      }
    };

    var formulaCell = {
      renderer: formulaRenderer,
      editor: Handsontable.editors.TextEditor,
      dataType: 'formula'
    };

    var textCell = {
      renderer: Handsontable.renderers.TextRenderer,
      editor: Handsontable.editors.TextEditor
    };

    var numericCell = {
      renderer: Handsontable.renderers.NumericRenderer,
      editor: Handsontable.editors.NumericEditor
    };

    this.init = function() {
      var instance = this;
      instance.formulasEnabled = !! instance.getSettings().formulas;

      if (instance.formulasEnabled) {

        var custom = {
          //
          // [MODIFICATION] (sci 2588)
          // Previously: "cellValue: instance.getDataAtCell"
          //
          cellValue: function(row, col){
              var value = instance.getDataAtCell(row, col);
              if (value && value[0] === '=') {
                  var formula = value.substr(1).toUpperCase();
                  var cellId = instance.plugin.utils.translateCellCoords({row: row, col: col});
                  var item = instance.plugin.matrix.getItem(cellId);

                  if (!item) {
                      item = instance.plugin.matrix.addItem({id: cellId, formula: formula});
                  } else {
                      item = instance.plugin.matrix.updateItem({id: cellId, formula: formula});
                  }
                  // parse formula
                  var newValue = instance.plugin.parse(formula, {row: row, col: col, id: cellId});
                  // cache result
                  formulasResults[cellId] = newValue;
                  // update item value and error
                  instance.plugin.matrix.updateItem(item, {formula: formula, value: newValue.result, error: newValue.error});

                  value = newValue.error || newValue.result;
              }
              return value;
          }
          // _!_MODIFICATION
        };

        instance.plugin = new ruleJS();
        instance.plugin.init();
        instance.plugin.custom = custom;

        Handsontable.cellTypes.registerCellType('formula', {
          editor: Handsontable.editors.TextEditor,
          renderer: formulaRenderer
        });

        Handsontable.cellTypes.text.renderer = formulaRenderer;
        Handsontable.cellTypes.numeric.renderer = formulaRenderer;

        // [MODIFICATION] (sci 2588)
        // This hook is new
        instance.addHook('beforeRender', beforeRender);
        // _!_MODIFICATION
        instance.addHook('afterChange', afterChange);
        instance.addHook('beforeAutofillInsidePopulate', beforeAutofillInsidePopulate);

        instance.addHook('afterCreateRow', afterCreateRow);
        instance.addHook('afterCreateCol', afterCreateCol);

      } else {
        // [MODIFICATION] (sci 2588)
        // This hook is new
        instance.removeHook('beforeRender', beforeRender);
        // _!_MODIFICATION
        instance.removeHook('afterChange', afterChange);
        instance.removeHook('beforeAutofillInsidePopulate', beforeAutofillInsidePopulate);

        instance.removeHook('afterCreateRow', afterCreateRow);
        instance.removeHook('afterCreateCol', afterCreateCol);
      }
    };
  }

  var htFormula = new HandsontableFormula();

  Handsontable.hooks.add('beforeInit', htFormula.init);

  Handsontable.hooks.add('afterUpdateSettings', function() {
    htFormula.init.call(this, 'afterUpdateSettings')
  });

})(Handsontable);
