'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?)');
      }

      window["ace"].require("ace/ext/language_tools");
      
      /**
       * 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) {
          window["ace"].config.set("basePath", window["Wizdom365AppUrl"] + "Base/Scripts/ace");

          var snippetManager = ace.require("ace/snippets").snippetManager;
          if (opts.snippets) {
              snippetManager.register(opts.snippets, opts.mode);
          }
          if (opts.onlyShowSnippetsCompletions) {
              acee.setOption("enableBasicAutocompletion", false);
              var langTools = ace.require("ace/ext/language_tools");
              langTools.setCompleters([langTools.snippetCompleter]); // disable locals from showing up in suggestions
          }

          // sets the ace worker path, if running from concatenated
          // or minified source
          if (angular.isDefined(opts.workerPath)) {
              var config = window.ace.require('ace/config');
              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);

          }
      };
  }]);