
import {registerPlugin} from './../../plugins';
import jsonpatch from 'jsonpatch';

export {ObserveChanges};

/**
 * @private
 * @plugin ObserveChanges
 * @dependencies jsonpatch
 */
function ObserveChanges() {}

Handsontable.hooks.add('afterLoadData', init);
Handsontable.hooks.add('afterUpdateSettings', init);

Handsontable.hooks.register('afterChangesObserved');

function init() {
  var instance = this;
  var pluginEnabled = instance.getSettings().observeChanges;

  if (pluginEnabled) {
    if (instance.observer) {
      destroy.call(instance); //destroy observer for old data object
    }
    createObserver.call(instance);
    bindEvents.call(instance);

  } else if (!pluginEnabled) {
    destroy.call(instance);
  }
}

function createObserver() {
  var instance = this;

  instance.observeChangesActive = true;

  instance.pauseObservingChanges = function() {
    instance.observeChangesActive = false;
  };

  instance.resumeObservingChanges = function() {
    instance.observeChangesActive = true;
  };

  instance.observedData = instance.getSourceData();
  instance.observer = jsonpatch.observe(instance.observedData, function(patches) {
    if (instance.observeChangesActive) {
      runHookForOperation.call(instance, patches);
      instance.render();
    }

    instance.runHooks('afterChangesObserved');
  });
}

function runHookForOperation(rawPatches) {
  var instance = this;
  var patches = cleanPatches(rawPatches);

  for (var i = 0, len = patches.length; i < len; i++) {
    var patch = patches[i];
    var parsedPath = parsePath(patch.path);

    if (!parsedPath) {
      return;
    }

    switch (patch.op) {
      case 'add':
        if (isNaN(parsedPath.col)) {
          instance.runHooks('afterCreateRow', parsedPath.row);
        } else {
          instance.runHooks('afterCreateCol', parsedPath.col);
        }
        break;

      case 'remove':
        if (isNaN(parsedPath.col)) {
          instance.runHooks('afterRemoveRow', parsedPath.row, 1);
        } else {
          instance.runHooks('afterRemoveCol', parsedPath.col, 1);
        }
        break;

      case 'replace':
        instance.runHooks('afterChange', [parsedPath.row, parsedPath.col, null, patch.value], 'external');
        break;
    }
  }

  function cleanPatches(rawPatches) {
    var patches;

    patches = removeLengthRelatedPatches(rawPatches);
    patches = removeMultipleAddOrRemoveColPatches(patches);

    return patches;
  }

  /**
   * Removing or adding column will produce one patch for each table row.
   * This function leaves only one patch for each column add/remove operation
   */
  function removeMultipleAddOrRemoveColPatches(rawPatches) {
    var newOrRemovedColumns = [];

    return rawPatches.filter(function(patch) {
      var parsedPath = parsePath(patch.path);

      if (!parsedPath) {
        return;
      }

      if (['add', 'remove'].indexOf(patch.op) != -1 && !isNaN(parsedPath.col)) {
        if (newOrRemovedColumns.indexOf(parsedPath.col) !== -1) {
          return false;
        }
        newOrRemovedColumns.push(parsedPath.col);
      }

      return true;
    });

  }

  /**
   * If observeChanges uses native Object.observe method, then it produces patches for length property.
   * This function removes them.
   */
  function removeLengthRelatedPatches(rawPatches) {
    return rawPatches.filter(function(patch) {
      return !/[/]length/ig.test(patch.path);
    });
  }

  function parsePath(path) {
    var match = path.match(/^\/(\d+)\/?(.*)?$/);

    if (!match) {
      return;
    }

    return {
      row: parseInt(match[1], 10),
      col: /^\d*$/.test(match[2]) ? parseInt(match[2], 10) : match[2]
    };
  }
}

function destroy() {
  var instance = this;

  if (instance.observer) {
    destroyObserver.call(instance);
    unbindEvents.call(instance);
  }
}

function destroyObserver() {
  var instance = this;

  jsonpatch.unobserve(instance.observedData, instance.observer);
  delete instance.observedData;
  delete instance.observeChangesActive;
  delete instance.pauseObservingChanges;
  delete instance.resumeObservingChanges;
}

function bindEvents() {
  var instance = this;
  instance.addHook('afterDestroy', destroy);

  instance.addHook('afterCreateRow', afterTableAlter);
  instance.addHook('afterRemoveRow', afterTableAlter);

  instance.addHook('afterCreateCol', afterTableAlter);
  instance.addHook('afterRemoveCol', afterTableAlter);

  instance.addHook('afterChange', function(changes, source) {
    if (source != 'loadData') {
      afterTableAlter.call(this);
    }
  });
}

function unbindEvents() {
  var instance = this;
  instance.removeHook('afterDestroy', destroy);

  instance.removeHook('afterCreateRow', afterTableAlter);
  instance.removeHook('afterRemoveRow', afterTableAlter);

  instance.removeHook('afterCreateCol', afterTableAlter);
  instance.removeHook('afterRemoveCol', afterTableAlter);

  instance.removeHook('afterChange', afterTableAlter);
}

function afterTableAlter() {
  var instance = this;

  instance.pauseObservingChanges();

  instance.addHookOnce('afterChangesObserved', function() {
    instance.resumeObservingChanges();
  });

}
