text: 'use strict';
  
  /**
   * Binds a ACE Editor widget
   */
  angular.module('ui.ace', [])
    .constant('uiAceConfig', {})
    .directive('uiAce', ['uiAceConfig', function (uiAceConfig) {
  
      if (angular.isUndefined(window.ace)) {
        throw new Error('ui-ace need ace to work... (o rly?)');
      }
  
      /**
       * Sets editor options such as the wrapping mode or the syntax checker.
       *
       * The supported options are:
       *
       *   <ul>
       *     <li>showGutter</li>
       *     <li>useWrapMode</li>
       *     <li>onLoad</li>
       *     <li>theme</li>
       *     <li>mode</li>
       *   </ul>
       *
       * @param acee
       * @param session ACE editor session
       * @param {object} opts Options to be set
       */
      var setOptions = function(acee, session, opts) {
  
        // sets the ace base and/or worker path, 
        // if running from concatenated or minified source
        if (angular.isDefined(opts.workerPath) || angular.isDefined(opts.basePath)) {
          var config = window.ace.require('ace/config');
          if (angular.isDefined(opts.basePath)) {
            config.set('basePath', opts.basePath);
          }
          if (angular.isDefined(opts.workerPath)) {
            config.set('workerPath', opts.workerPath);
          }
        }
        // ace requires loading
        if (angular.isDefined(opts.require)) {
          opts.require.forEach(function (n) {
              window.ace.require(n);
          });
        }
        // Boolean options
        if (angular.isDefined(opts.showGutter)) {
          acee.renderer.setShowGutter(opts.showGutter);
        }
        if (angular.isDefined(opts.useWrapMode)) {
          session.setUseWrapMode(opts.useWrapMode);
        }
        if (angular.isDefined(opts.showInvisibles)) {
          acee.renderer.setShowInvisibles(opts.showInvisibles);
        }
        if (angular.isDefined(opts.showIndentGuides)) {
          acee.renderer.setDisplayIndentGuides(opts.showIndentGuides);
        }
        if (angular.isDefined(opts.useSoftTabs)) {
          session.setUseSoftTabs(opts.useSoftTabs);
        }
        if (angular.isDefined(opts.showPrintMargin)) {
          acee.setShowPrintMargin(opts.showPrintMargin);
        }
  
        // commands
        if (angular.isDefined(opts.disableSearch) && opts.disableSearch) {
          acee.commands.addCommands([
            {
              name: 'unfind',
              bindKey: {
                win: 'Ctrl-F',
                mac: 'Command-F'
              },
              exec: function () {
                return false;
              },
              readOnly: true
            }
          ]);
        }
  
        // Basic options
        if (angular.isString(opts.theme)) {
          acee.setTheme('ace/theme/' + opts.theme);
        }
        if (angular.isString(opts.mode)) {
          session.setMode('ace/mode/' + opts.mode);
        }
        // Advanced options
        if (angular.isDefined(opts.firstLineNumber)) {
          if (angular.isNumber(opts.firstLineNumber)) {
            session.setOption('firstLineNumber', opts.firstLineNumber);
          } else if (angular.isFunction(opts.firstLineNumber)) {
            session.setOption('firstLineNumber', opts.firstLineNumber());
          }
        }
  
        // advanced options
        var key, obj;
        if (angular.isDefined(opts.advanced)) {
            for (key in opts.advanced) {
                // create a javascript object with the key and value
                obj = { name: key, value: opts.advanced[key] };
                // try to assign the option to the ace editor
                acee.setOption(obj.name, obj.value);
            }
        }
  
        // advanced options for the renderer
        if (angular.isDefined(opts.rendererOptions)) {
            for (key in opts.rendererOptions) {
                // create a javascript object with the key and value
                obj = { name: key, value: opts.rendererOptions[key] };
                // try to assign the option to the ace editor
                acee.renderer.setOption(obj.name, obj.value);
            }
        }
  
        // onLoad callbacks
        angular.forEach(opts.callbacks, function (cb) {
          if (angular.isFunction(cb)) {
            cb(acee);
          }
        });
      };
  
      return {
        restrict: 'EA',
        require: '?ngModel',
        link: function (scope, elm, attrs, ngModel) {
  
          /**
           * Corresponds the uiAceConfig ACE configuration.
           * @type object
           */
          var options = uiAceConfig.ace || {};
  
          /**
           * uiAceConfig merged with user options via json in attribute or data binding
           * @type object
           */
          var opts = angular.extend({}, options, scope.$eval(attrs.uiAce));
  
          /**
           * ACE editor
           * @type object
           */
          var acee = window.ace.edit(elm[0]);
  
          /**
           * ACE editor session.
           * @type object
           * @see [EditSession]{@link http://ace.c9.io/#nav=api&api=edit_session}
           */
          var session = acee.getSession();
  
          /**
           * Reference to a change listener created by the listener factory.
           * @function
           * @see listenerFactory.onChange
           */
          var onChangeListener;
  
          /**
           * Reference to a blur listener created by the listener factory.
           * @function
           * @see listenerFactory.onBlur
           */
          var onBlurListener;
  
          /**
           * Calls a callback by checking its existing. The argument list
           * is variable and thus this function is relying on the arguments
           * object.
           * @throws {Error} If the callback isn't a function
           */
          var executeUserCallback = function () {
  
            /**
             * The callback function grabbed from the array-like arguments
             * object. The first argument should always be the callback.
             *
             * @see [arguments]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments}
             * @type {*}
             */
            var callback = arguments[0];
  
            /**
             * Arguments to be passed to the callback. These are taken
             * from the array-like arguments object. The first argument
             * is stripped because that should be the callback function.
             *
             * @see [arguments]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments}
             * @type {Array}
             */
            var args = Array.prototype.slice.call(arguments, 1);
  
            if (angular.isDefined(callback)) {
              scope.$evalAsync(function () {
                if (angular.isFunction(callback)) {
                  callback(args);
                } else {
                  throw new Error('ui-ace use a function as callback.');
                }
              });
            }
          };
  
          /**
           * Listener factory. Until now only change listeners can be created.
           * @type object
           */
          var listenerFactory = {
            /**
             * Creates a change listener which propagates the change event
             * and the editor session to the callback from the user option
             * onChange. It might be exchanged during runtime, if this
             * happens the old listener will be unbound.
             *
             * @param callback callback function defined in the user options
             * @see onChangeListener
             */
            onChange: function (callback) {
              return function (e) {
                var newValue = session.getValue();
  
                if (ngModel && newValue !== ngModel.$viewValue &&
                    // HACK make sure to only trigger the apply outside of the
                    // digest loop 'cause ACE is actually using this callback
                    // for any text transformation !
                    !scope.$$phase && !scope.$root.$$phase) {
                  scope.$evalAsync(function () {
                    ngModel.$setViewValue(newValue);
                  });
                }
  
                executeUserCallback(callback, e, acee);
              };
            },
            /**
             * Creates a blur listener which propagates the editor session
             * to the callback from the user option onBlur. It might be
             * exchanged during runtime, if this happens the old listener
             * will be unbound.
             *
             * @param callback callback function defined in the user options
             * @see onBlurListener
             */
            onBlur: function (callback) {
              return function () {
                executeUserCallback(callback, acee);
              };
            }
          };
  
          attrs.$observe('readonly', function (value) {
            acee.setReadOnly(!!value || value === '');
          });
  
          // Value Blind
          if (ngModel) {
            ngModel.$formatters.push(function (value) {
              if (angular.isUndefined(value) || value === null) {
                return '';
              }
              else if (angular.isObject(value) || angular.isArray(value)) {
                throw new Error('ui-ace cannot use an object or an array as a model');
              }
              return value;
            });
  
            ngModel.$render = function () {
              session.setValue(ngModel.$viewValue);
            };
          }
  
          // Listen for option updates
          var updateOptions = function (current, previous) {
            if (current === previous) return;
            opts = angular.extend({}, options, scope.$eval(attrs.uiAce));
  
            opts.callbacks = [ opts.onLoad ];
            if (opts.onLoad !== options.onLoad) {
              // also call the global onLoad handler
              opts.callbacks.unshift(options.onLoad);
            }
  
            // EVENTS
  
            // unbind old change listener
            session.removeListener('change', onChangeListener);
  
            // bind new change listener
            onChangeListener = listenerFactory.onChange(opts.onChange);
            session.on('change', onChangeListener);
  
            // unbind old blur listener
            //session.removeListener('blur', onBlurListener);
            acee.removeListener('blur', onBlurListener);
  
            // bind new blur listener
            onBlurListener = listenerFactory.onBlur(opts.onBlur);
            acee.on('blur', onBlurListener);
  
            setOptions(acee, session, opts);
          };
  
          scope.$watch(attrs.uiAce, updateOptions, /* deep watch */ true);
  
          // set the options here, even if we try to watch later, if this
          // line is missing things go wrong (and the tests will also fail)
          updateOptions(options);
  
          elm.on('$destroy', function () {
            acee.session.$stopWorker();
            acee.destroy();
          });
  
          scope.$watch(function() {
            return [elm[0].offsetWidth, elm[0].offsetHeight];
          }, function() {
            acee.resize();
            acee.renderer.updateFull();
          }, true);
  
        }
      };
    }]);
  