/**
 * ng-handsontable 0.13.0
 * 
 * Copyright 2012-2015 Marcin Warpechowski
 * Copyright 2015 Handsoncode sp. z o.o. <hello@handsontable.com>
 * Licensed under the MIT license.
 * https://github.com/handsontable/ngHandsontable
 * Date: Wed Oct 26 2016 10:00:05 GMT+0200 (CEST)
*/

if (document.all && !document.addEventListener) { // IE 8 and lower
  document.createElement('hot-table');
  document.createElement('hot-column');
  document.createElement('hot-autocomplete');
}

angular.module('ngHandsontable.services', []);
angular.module('ngHandsontable.directives', []);
angular.module('ngHandsontable', [
    'ngHandsontable.services',
    'ngHandsontable.directives'
  ]);

Handsontable.hooks.add('afterContextMenuShow', function() {
  Handsontable.eventManager.isHotTableEnv = false;
});

(function() {
  function autoCompleteFactory($parse) {
    return {
      parseAutoComplete: function(column, dataSet, propertyOnly) {
        column.source = function(query, process) {
          var row = this.instance.getSelected()[0];
          var source = [];
          var data = dataSet[row];

          if (!data) {
            return;
          }
          var options = column.optionList;

          if (!options || !options.object) {
            return;
          }
          if (angular.isArray(options.object)) {
            source = options.object;
          } else {
            // Using $parse to evaluate the expression against the row object
            // allows us to support filters like the ngRepeat directive does.
            var paramObject = $parse(options.object)(data);

            if (angular.isArray(paramObject)) {
              if (propertyOnly) {
                for (var i = 0, length = paramObject.length; i < length; i++) {
                  var item = paramObject[i][options.property];

                  if (item !== null && item !== undefined) {
                    source.push(item);
                  }
                }
              } else {
                source = paramObject;
              }
            } else {
              source = paramObject;
            }
          }
          process(source);
        };
      }
    };
  }
  autoCompleteFactory.$inject = ['$parse'];

  angular.module('ngHandsontable.services').factory('autoCompleteFactory', autoCompleteFactory);
}());

(function() {

  function hotRegisterer() {
    var instances = {};

    return {
      getInstance: function(id) {
        return instances[id];
      },

      registerInstance: function(id, instance) {
        instances[id] = instance;
      },

      removeInstance: function(id) {
        instances[id] = void 0;
      }
    };
  }
  hotRegisterer.$inject = [];

  angular.module('ngHandsontable.services').factory('hotRegisterer', hotRegisterer);
}());

(function() {

  function hyphenate(string) {
    return string.replace(/[A-Z]/g, function(match) {
      return ('-' + match.charAt(0).toLowerCase());
    });
  }

  function camelCase(string) {
    return string.replace(/-\D/g, function(match) {
      return match.charAt(1).toUpperCase();
    });
  }

  function ucFirst(string) {
    return string.substr(0, 1).toUpperCase() + string.substr(1, string.length - 1);
  }

  function settingFactory(hotRegisterer) {
    return {
      containerClassName: 'handsontable-container',

      /**
       * Append handsontable container div and initialize handsontable instance inside element.
       *
       * @param {qLite} element
       * @param {Object} htSettings
       */
      initializeHandsontable: function(element, htSettings) {
        var container = document.createElement('div'),
          hot;

        container.className = this.containerClassName;

        if (htSettings.hotId) {
          container.id = htSettings.hotId;
        }
        element[0].appendChild(container);
        hot = new Handsontable(container, htSettings);

        if (htSettings.hotId) {
          hotRegisterer.registerInstance(htSettings.hotId, hot);
        }

        return hot;
      },

      /**
       * Set new settings to handsontable instance.
       *
       * @param {Handsontable} instance
       * @param {Object} settings
       */
      updateHandsontableSettings: function(instance, settings) {
        if (instance) {
          instance.updateSettings(settings);
        }
      },

      /**
       * Render handsontable instance inside element.
       *
       * @param {Handsontable} instance
       */
      renderHandsontable: function(instance) {
        if (instance) {
          instance.render();
        }
      },

      /**
       * Merge original handsontable settings with setting defined in scope.
       *
       * @param {Object} settings
       * @param {Object} scope
       * @returns {Object}
       */
      mergeSettingsFromScope: function(settings, scope) {
        var
          scopeOptions = angular.extend({}, scope),
          htOptions, i, length;

        settings = settings || {};
        angular.extend(scopeOptions, scope.settings || {});
        htOptions = this.getAvailableSettings();

        for (i = 0, length = htOptions.length; i < length; i++) {
          if (typeof scopeOptions[htOptions[i]] !== 'undefined') {
            settings[htOptions[i]] = scopeOptions[htOptions[i]];
          }
        }

        return settings;
      },

      /**
       * Merge original handsontable hooks with hooks defined in scope.
       *
       * @param {Object} settings
       * @param {Object} scope
       * @returns {Object}
       */
      mergeHooksFromScope: function(settings, scope) {
        var
          scopeOptions = angular.extend({}, scope),
          htHooks, i, length, attribute;

        settings = settings || {};
        angular.extend(scopeOptions, scope.settings || {});
        htHooks = this.getAvailableHooks();

        for (i = 0, length = htHooks.length; i < length; i++) {
          attribute = 'on' + ucFirst(htHooks[i]);

          if (typeof scopeOptions[htHooks[i]] === 'function' || typeof scopeOptions[attribute] === 'function') {
            settings[htHooks[i]] = scopeOptions[htHooks[i]] || scopeOptions[attribute];
          }
        }

        return settings;
      },

      /**
       * Trim scope definition according to attrs object from directive.
       *
       * @param {Object} scopeDefinition
       * @param {Object} attrs
       * @returns {Object}
       */
      trimScopeDefinitionAccordingToAttrs: function(scopeDefinition, attrs) {
        for (var i in scopeDefinition) {
          if (scopeDefinition.hasOwnProperty(i) && attrs[i] === void 0 &&
              attrs[scopeDefinition[i].substr(1, scopeDefinition[i].length)] === void 0) {
            delete scopeDefinition[i];
          }
        }

        return scopeDefinition;
      },

      /**
       * Get isolate scope definition for main handsontable directive.
       *
       * @return {Object}
       */
      getTableScopeDefinition: function() {
        var scopeDefinition = {};

        this.applyAvailableSettingsScopeDef(scopeDefinition);
        this.applyAvailableHooksScopeDef(scopeDefinition);

        scopeDefinition.datarows = '=';
        scopeDefinition.dataschema = '=';
        scopeDefinition.observeDomVisibility = '=';
        //scopeDefinition.settings = '=';

        return scopeDefinition;
      },

      /**
       * Get isolate scope definition for column directive.
       *
       * @return {Object}
       */
      getColumnScopeDefinition: function() {
        var scopeDefinition = {};

        this.applyAvailableSettingsScopeDef(scopeDefinition);
        scopeDefinition.data = '@';

        return scopeDefinition;
      },

      /**
       * Apply all available handsontable settings into object which represents scope definition.
       *
       * @param {Object} [scopeDefinition]
       * @returns {Object}
       */
      applyAvailableSettingsScopeDef: function(scopeDefinition) {
        var options, i, length;

        options = this.getAvailableSettings();

        for (i = 0, length = options.length; i < length; i++) {
          scopeDefinition[options[i]] = '=';
        }

        return scopeDefinition;
      },

      /**
       * Apply all available handsontable hooks into object which represents scope definition.
       *
       * @param {Object} [scopeDefinition]
       * @returns {Object}
       */
      applyAvailableHooksScopeDef: function(scopeDefinition) {
        var options, i, length;

        options = this.getAvailableHooks();

        for (i = 0, length = options.length; i < length; i++) {
          scopeDefinition[options[i]] = '=on' + ucFirst(options[i]);
        }

        return scopeDefinition;
      },

      /**
       * Get all available settings from handsontable, returns settings by default in camelCase mode.
       *
       * @param {Boolean} [hyphenateStyle=undefined] If `true` then returns options in hyphenate mode (eq. row-header)
       * @returns {Array}
       */
      getAvailableSettings: function(hyphenateStyle) {
        var settings = Object.keys(Handsontable.DefaultSettings.prototype);

        if (settings.indexOf('contextMenuCopyPaste') === -1) {
          settings.push('contextMenuCopyPaste');
        }
        if (settings.indexOf('handsontable') === -1) {
          settings.push('handsontable');
        }
        if (settings.indexOf('settings') >= 0) {
          settings.splice(settings.indexOf('settings'), 1);
        }
        if (hyphenateStyle) {
          settings = settings.map(hyphenate);
        }

        return settings;
      },

      /**
       * Get all available hooks from handsontable, returns hooks by default in camelCase mode.
       *
       * @param {Boolean} [hyphenateStyle=undefined] If `true` then returns hooks in hyphenate mode (eq. on-after-init)
       * @returns {Array}
       */
      getAvailableHooks: function(hyphenateStyle) {
        var settings = Handsontable.hooks.getRegistered();

        if (hyphenateStyle) {
          settings = settings.map(function(hook) {
            return 'on-' + hyphenate(hook);
          });
        }

        return settings;
      }
    };
  }
  settingFactory.$inject = ['hotRegisterer'];

  angular.module('ngHandsontable.services').factory('settingFactory', settingFactory);
}());

(function() {
  /**
   * Angular Handsontable directive for autocomplete settings
   */
  function hotAutocomplete() {
    return {
      restrict: 'EA',
      scope: true,
      require: '^hotColumn',
      link: function(scope, element, attrs, controllerInstance) {
        var options = attrs.datarows;

        controllerInstance.setColumnOptionList(options);
      }
    };
  }
  hotAutocomplete.$inject = [];

  angular.module('ngHandsontable.directives').directive('hotAutocomplete', hotAutocomplete);
}());

(function() {
  /**
   * Angular Handsontable directive for single column settings
   */
  function hotColumn(settingFactory) {
    return {
      restrict: 'EA',
      require: '^hotTable',
      scope: {},
      controller: ['$scope', function($scope) {
        this.setColumnOptionList = function(options) {
          if (!$scope.column) {
            $scope.column = {};
          }
          var optionList = {};
          var match = options.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)\s*$/);

          if (match) {
            optionList.property = match[1];
            optionList.object = match[2];
          } else {
            optionList.object = options.split(',');
          }
          $scope.column.optionList = optionList;
        };
      }],
      compile: function(tElement, tAttrs) {
        var _this = this;

        this.scope = settingFactory.trimScopeDefinitionAccordingToAttrs(settingFactory.getColumnScopeDefinition(), tAttrs);
        //this.$$isolateBindings = {};

        angular.forEach(Object.keys(this.scope), function(key) {
          _this.$$isolateBindings[key] = {
            attrName: key,
            collection: false,
            mode: key === 'data' ? '@' : '=',
            optional: false
          };
        });

        return function(scope, element, attrs, controllerInstance) {
          var column = {};

          // Turn all attributes without value as `true` by default
          angular.forEach(Object.keys(attrs), function(key) {
            if (key.charAt(0) !== '$' && attrs[key] === '') {
              column[key] = true;
            }
          });
          settingFactory.mergeSettingsFromScope(column, scope);

          if (!scope.column) {
            scope.column = {};
          }
          angular.extend(scope.column, column);
          controllerInstance.setColumnSetting(scope.column);

          scope.$on('$destroy', function() {
            controllerInstance.removeColumnSetting(scope.column);
          });
        };
      }
    };
  }
  hotColumn.$inject = ['settingFactory'];

  angular.module('ngHandsontable.directives').directive('hotColumn', hotColumn);
}());

(function() {
  /**
   * Main Angular Handsontable directive
   */
  function hotTable(settingFactory, autoCompleteFactory, $rootScope, $parse) {
    return {
      restrict: 'EA',
      scope: {},
      // for ng-repeat
      priority: -400,
      controller: ['$scope', function($scope) {
        this.setColumnSetting = function(column) {
          if (!$scope.htSettings) {
            $scope.htSettings = {};
          }
          if (!$scope.htSettings.columns) {
            $scope.htSettings.columns = [];
          }
          $scope.htSettings.columns.push(column);
          settingFactory.updateHandsontableSettings($scope.hotInstance, $scope.htSettings);
        };
        this.removeColumnSetting = function(column) {
          if ($scope.htSettings.columns.indexOf(column) > -1) {
            $scope.htSettings.columns.splice($scope.htSettings.columns.indexOf(column), 1);
            settingFactory.updateHandsontableSettings($scope.hotInstance, $scope.htSettings);
          }
        };
      }],
      compile: function(tElement, tAttrs) {
        var _this = this,
          bindingsKeys;

        this.scope = settingFactory.trimScopeDefinitionAccordingToAttrs(settingFactory.getTableScopeDefinition(), tAttrs);
        bindingsKeys = Object.keys(this.scope);

        angular.forEach(bindingsKeys, function(key) {
          var mode = _this.scope[key].charAt(0);

          _this.$$isolateBindings[key] = {
            attrName: _this.scope[key].length > 1 ? _this.scope[key].substr(1, _this.scope[key].length) : key,
            collection: key === 'datarows',
            mode: mode,
            optional: false
          };
        });

        return function(scope, element, attrs) {
          scope.settings = $parse(attrs.settings)(scope.$parent);

          if (!scope.htSettings) {
            scope.htSettings = {};
          }
          // Turn all attributes without value as `true` by default
          angular.forEach(Object.keys(attrs), function(key) {
            if (key.charAt(0) !== '$' && attrs[key] === '') {
              scope.htSettings[key] = true;
            }
          });

          settingFactory.mergeSettingsFromScope(scope.htSettings, scope);
          settingFactory.mergeHooksFromScope(scope.htSettings, scope);

          if (!scope.htSettings.data) {
            scope.htSettings.data = scope.datarows;
          }
          scope.htSettings.dataSchema = scope.dataschema;
          scope.htSettings.hotId = attrs.hotId;
          scope.htSettings.observeDOMVisibility = scope.observeDomVisibility;

          if (scope.htSettings.columns) {
            for (var i = 0, length = scope.htSettings.columns.length; i < length; i++) {
              var column = scope.htSettings.columns[i];

              if (column.type !== 'autocomplete') {
                continue;
              }
              if (!column.optionList) {
                continue;
              }
              if (typeof column.optionList === 'string') {
                var optionList = {};
                var match = column.optionList.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)\s*$/);

                if (match) {
                  optionList.property = match[1];
                  optionList.object = match[2];
                } else {
                  optionList.object = optionList;
                }
                column.optionList = optionList;
              }
              autoCompleteFactory.parseAutoComplete(column, scope.datarows, true);
            }
          }
          var origAfterChange = scope.htSettings.afterChange;

          scope.htSettings.afterChange = function() {
            if (origAfterChange) {
              origAfterChange.apply(this, arguments);
            }
            if (!$rootScope.$$phase) {
              scope.$apply();
            }
          };
          scope.hotInstance = settingFactory.initializeHandsontable(element, scope.htSettings);

          // TODO: Add watch properties descriptor + needs perf test. Watch full equality vs toJson
          angular.forEach(bindingsKeys, function(key) {
            scope.$watch(key, function(newValue, oldValue) {
              if (newValue === void 0) {
                return;
              }
              if (key === 'datarows') {
                // If reference to data rows is not changed then only re-render table
                if (scope.hotInstance.getSettings().data === newValue) {
                  settingFactory.renderHandsontable(scope.hotInstance);
                } else {
                  scope.hotInstance.loadData(newValue);
                  scope.htSettings.data = newValue;
                }
              } else if (newValue !== oldValue) {
                scope.htSettings[key] = newValue;
                settingFactory.updateHandsontableSettings(scope.hotInstance, scope.htSettings);
              }
            }, ['datarows', 'columns', 'rowHeights', 'colWidths', 'rowHeaders', 'colHeaders'].indexOf(key) >= 0);
          });

          /**
           * Check for reference equality changes for datarows
           * TODO: must the remaining bindingsKeys need to be added also if their reference changes
           */
          scope.$watch('datarows', function(newValue) {
            if (newValue === void 0) {
              return;
            }
            if (scope.hotInstance.getSettings().data !== newValue) {
              scope.hotInstance.loadData(newValue);
            }
          });

          /**
           * Check if data length has been changed
           */
          scope.$watchCollection('datarows', function(newValue, oldValue) {
            if (oldValue && oldValue.length === scope.htSettings.minSpareRows && newValue.length !== scope.htSettings.minSpareRows) {
              scope.htSettings.data = scope.datarows;
              settingFactory.updateHandsontableSettings(scope.hotInstance, scope.htSettings);
            }
          });
        };
      }
    };
  }
  hotTable.$inject = ['settingFactory', 'autoCompleteFactory', '$rootScope', '$parse'];

  angular.module('ngHandsontable.directives').directive('hotTable', hotTable);
}());
