text: define([
    'jquery',
    './options',
    './utils',
    './keys'
  ], function ($, Options, Utils, KEYS) {
    var Select2 = function ($element, options) {
      if ($element.data('select2') != null) {
        $element.data('select2').destroy();
      }
  
      this.$element = $element;
  
      this.id = this._generateId($element);
  
      options = options || {};
  
      this.options = new Options(options, $element);
  
      Select2.__super__.constructor.call(this);
  
      // Set up the tabindex
  
      var tabindex = $element.attr('tabindex') || 0;
      $element.data('old-tabindex', tabindex);
      $element.attr('tabindex', '-1');
  
      // Set up containers and adapters
  
      var DataAdapter = this.options.get('dataAdapter');
      this.dataAdapter = new DataAdapter($element, this.options);
  
      var $container = this.render();
  
      this._placeContainer($container);
  
      var SelectionAdapter = this.options.get('selectionAdapter');
      this.selection = new SelectionAdapter($element, this.options);
      this.$selection = this.selection.render();
  
      this.selection.position(this.$selection, $container);
  
      var DropdownAdapter = this.options.get('dropdownAdapter');
      this.dropdown = new DropdownAdapter($element, this.options);
      this.$dropdown = this.dropdown.render();
  
      this.dropdown.position(this.$dropdown, $container);
  
      var ResultsAdapter = this.options.get('resultsAdapter');
      this.results = new ResultsAdapter($element, this.options, this.dataAdapter);
      this.$results = this.results.render();
  
      this.results.position(this.$results, this.$dropdown);
  
      // Bind events
  
      var self = this;
  
      // Bind the container to all of the adapters
      this._bindAdapters();
  
      // Register any DOM event handlers
      this._registerDomEvents();
  
      // Register any internal event handlers
      this._registerDataEvents();
      this._registerSelectionEvents();
      this._registerDropdownEvents();
      this._registerResultsEvents();
      this._registerEvents();
  
      // Set the initial state
      this.dataAdapter.current(function (initialData) {
        self.trigger('selection:update', {
          data: initialData
        });
      });
  
      // Hide the original select
      $element.addClass('select2-hidden-accessible');
      $element.attr('aria-hidden', 'true');
  
      // Synchronize any monitored attributes
      this._syncAttributes();
  
      $element.data('select2', this);
    };
  
    Utils.Extend(Select2, Utils.Observable);
  
    Select2.prototype._generateId = function ($element) {
      var id = '';
  
      if ($element.attr('id') != null) {
        id = $element.attr('id');
      } else if ($element.attr('name') != null) {
        id = $element.attr('name') + '-' + Utils.generateChars(2);
      } else {
        id = Utils.generateChars(4);
      }
  
      id = id.replace(/(:|\.|\[|\]|,)/g, '');
      id = 'select2-' + id;
  
      return id;
    };
  
    Select2.prototype._placeContainer = function ($container) {
      $container.insertAfter(this.$element);
  
      var width = this._resolveWidth(this.$element, this.options.get('width'));
  
      if (width != null) {
        $container.css('width', width);
      }
    };
  
    Select2.prototype._resolveWidth = function ($element, method) {
      var WIDTH = /^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i;
  
      if (method == 'resolve') {
        var styleWidth = this._resolveWidth($element, 'style');
  
        if (styleWidth != null) {
          return styleWidth;
        }
  
        return this._resolveWidth($element, 'element');
      }
  
      if (method == 'element') {
        var elementWidth = $element.outerWidth(false);
  
        if (elementWidth <= 0) {
          return 'auto';
        }
  
        return elementWidth + 'px';
      }
  
      if (method == 'style') {
        var style = $element.attr('style');
  
        if (typeof(style) !== 'string') {
          return null;
        }
  
        var attrs = style.split(';');
  
        for (var i = 0, l = attrs.length; i < l; i = i + 1) {
          var attr = attrs[i].replace(/\s/g, '');
          var matches = attr.match(WIDTH);
  
          if (matches !== null && matches.length >= 1) {
            return matches[1];
          }
        }
  
        return null;
      }
  
      return method;
    };
  
    Select2.prototype._bindAdapters = function () {
      this.dataAdapter.bind(this, this.$container);
      this.selection.bind(this, this.$container);
  
      this.dropdown.bind(this, this.$container);
      this.results.bind(this, this.$container);
    };
  
    Select2.prototype._registerDomEvents = function () {
      var self = this;
  
      this.$element.on('change.select2', function () {
        self.dataAdapter.current(function (data) {
          self.trigger('selection:update', {
            data: data
          });
        });
      });
  
      this._sync = Utils.bind(this._syncAttributes, this);
  
      if (this.$element[0].attachEvent) {
        this.$element[0].attachEvent('onpropertychange', this._sync);
      }
  
      var observer = window.MutationObserver ||
        window.WebKitMutationObserver ||
        window.MozMutationObserver
      ;
  
      if (observer != null) {
        this._observer = new observer(function (mutations) {
          $.each(mutations, self._sync);
        });
        this._observer.observe(this.$element[0], {
          attributes: true,
          subtree: false
        });
      } else if (this.$element[0].addEventListener) {
        this.$element[0].addEventListener('DOMAttrModified', self._sync, false);
      }
    };
  
    Select2.prototype._registerDataEvents = function () {
      var self = this;
  
      this.dataAdapter.on('*', function (name, params) {
        self.trigger(name, params);
      });
    };
  
    Select2.prototype._registerSelectionEvents = function () {
      var self = this;
      var nonRelayEvents = ['toggle', 'focus'];
  
      this.selection.on('toggle', function () {
        self.toggleDropdown();
      });
  
      this.selection.on('focus', function (params) {
        self.focus(params);
      });
  
      this.selection.on('*', function (name, params) {
        if ($.inArray(name, nonRelayEvents) !== -1) {
          return;
        }
  
        self.trigger(name, params);
      });
    };
  
    Select2.prototype._registerDropdownEvents = function () {
      var self = this;
  
      this.dropdown.on('*', function (name, params) {
        self.trigger(name, params);
      });
    };
  
    Select2.prototype._registerResultsEvents = function () {
      var self = this;
  
      this.results.on('*', function (name, params) {
        self.trigger(name, params);
      });
    };
  
    Select2.prototype._registerEvents = function () {
      var self = this;
  
      this.on('open', function () {
        self.$container.addClass('select2-container--open');
      });
  
      this.on('close', function () {
        self.$container.removeClass('select2-container--open');
      });
  
      this.on('enable', function () {
        self.$container.removeClass('select2-container--disabled');
      });
  
      this.on('disable', function () {
        self.$container.addClass('select2-container--disabled');
      });
  
      this.on('blur', function () {
        self.$container.removeClass('select2-container--focus');
      });
  
      this.on('query', function (params) {
        if (!self.isOpen()) {
          self.trigger('open', {});
        }
  
        this.dataAdapter.query(params, function (data) {
          self.trigger('results:all', {
            data: data,
            query: params
          });
        });
      });
  
      this.on('query:append', function (params) {
        this.dataAdapter.query(params, function (data) {
          self.trigger('results:append', {
            data: data,
            query: params
          });
        });
      });
  
      this.on('keypress', function (evt) {
        var key = evt.which;
  
        if (self.isOpen()) {
          if (key === KEYS.ESC || key === KEYS.TAB ||
              (key === KEYS.UP && evt.altKey)) {
            self.close();
  
            evt.preventDefault();
          } else if (key === KEYS.ENTER) {
            self.trigger('results:select', {});
  
            evt.preventDefault();
          } else if ((key === KEYS.SPACE && evt.ctrlKey)) {
            self.trigger('results:toggle', {});
  
            evt.preventDefault();
          } else if (key === KEYS.UP) {
            self.trigger('results:previous', {});
  
            evt.preventDefault();
          } else if (key === KEYS.DOWN) {
            self.trigger('results:next', {});
  
            evt.preventDefault();
          }
        } else {
          if (key === KEYS.ENTER || key === KEYS.SPACE ||
              (key === KEYS.DOWN && evt.altKey)) {
            self.open();
  
            evt.preventDefault();
          }
        }
      });
    };
  
    Select2.prototype._syncAttributes = function () {
      this.options.set('disabled', this.$element.prop('disabled'));
  
      if (this.options.get('disabled')) {
        if (this.isOpen()) {
          this.close();
        }
  
        this.trigger('disable', {});
      } else {
        this.trigger('enable', {});
      }
    };
  
    /**
     * Override the trigger method to automatically trigger pre-events when
     * there are events that can be prevented.
     */
    Select2.prototype.trigger = function (name, args) {
      var actualTrigger = Select2.__super__.trigger;
      var preTriggerMap = {
        'open': 'opening',
        'close': 'closing',
        'select': 'selecting',
        'unselect': 'unselecting'
      };
  
      if (args === undefined) {
        args = {};
      }
  
      if (name in preTriggerMap) {
        var preTriggerName = preTriggerMap[name];
        var preTriggerArgs = {
          prevented: false,
          name: name,
          args: args
        };
  
        actualTrigger.call(this, preTriggerName, preTriggerArgs);
  
        if (preTriggerArgs.prevented) {
          args.prevented = true;
  
          return;
        }
      }
  
      actualTrigger.call(this, name, args);
    };
  
    Select2.prototype.toggleDropdown = function () {
      if (this.options.get('disabled')) {
        return;
      }
  
      if (this.isOpen()) {
        this.close();
      } else {
        this.open();
      }
    };
  
    Select2.prototype.open = function () {
      if (this.isOpen()) {
        return;
      }
  
      this.trigger('query', {});
    };
  
    Select2.prototype.close = function () {
      if (!this.isOpen()) {
        return;
      }
  
      this.trigger('close', {});
    };
  
    Select2.prototype.isOpen = function () {
      return this.$container.hasClass('select2-container--open');
    };
  
    Select2.prototype.hasFocus = function () {
      return this.$container.hasClass('select2-container--focus');
    };
  
    Select2.prototype.focus = function (data) {
      // No need to re-trigger focus events if we are already focused
      if (this.hasFocus()) {
        return;
      }
  
      this.$container.addClass('select2-container--focus');
      this.trigger('focus', {});
    };
  
    Select2.prototype.enable = function (args) {
      if (this.options.get('debug') && window.console && console.warn) {
        console.warn(
          'Select2: The `select2("enable")` method has been deprecated and will' +
          ' be removed in later Select2 versions. Use $element.prop("disabled")' +
          ' instead.'
        );
      }
  
      if (args == null || args.length === 0) {
        args = [true];
      }
  
      var disabled = !args[0];
  
      this.$element.prop('disabled', disabled);
    };
  
    Select2.prototype.data = function () {
      if (this.options.get('debug') &&
          arguments.length > 0 && window.console && console.warn) {
        console.warn(
          'Select2: Data can no longer be set using `select2("data")`. You ' +
          'should consider setting the value instead using `$element.val()`.'
        );
      }
  
      var data = [];
  
      this.dataAdapter.current(function (currentData) {
        data = currentData;
      });
  
      return data;
    };
  
    Select2.prototype.val = function (args) {
      if (this.options.get('debug') && window.console && console.warn) {
        console.warn(
          'Select2: The `select2("val")` method has been deprecated and will be' +
          ' removed in later Select2 versions. Use $element.val() instead.'
        );
      }
  
      if (args == null || args.length === 0) {
        return this.$element.val();
      }
  
      var newVal = args[0];
  
      if ($.isArray(newVal)) {
        newVal = $.map(newVal, function (obj) {
          return obj.toString();
        });
      }
  
      this.$element.val(newVal).trigger('change');
    };
  
    Select2.prototype.destroy = function () {
      this.$container.remove();
  
      if (this.$element[0].detachEvent) {
        this.$element[0].detachEvent('onpropertychange', this._sync);
      }
  
      if (this._observer != null) {
        this._observer.disconnect();
        this._observer = null;
      } else if (this.$element[0].removeEventListener) {
        this.$element[0]
          .removeEventListener('DOMAttrModified', this._sync, false);
      }
  
      this._sync = null;
  
      this.$element.off('.select2');
      this.$element.attr('tabindex', this.$element.data('old-tabindex'));
  
      this.$element.removeClass('select2-hidden-accessible');
      this.$element.attr('aria-hidden', 'false');
      this.$element.removeData('select2');
  
      this.dataAdapter.destroy();
      this.selection.destroy();
      this.dropdown.destroy();
      this.results.destroy();
  
      this.dataAdapter = null;
      this.selection = null;
      this.dropdown = null;
      this.results = null;
    };
  
    Select2.prototype.render = function () {
      var $container = $(
        '<span class="select2 select2-container">' +
          '<span class="selection"></span>' +
          '<span class="dropdown-wrapper" aria-hidden="true"></span>' +
        '</span>'
      );
  
      $container.attr('dir', this.options.get('dir'));
  
      this.$container = $container;
  
      this.$container.addClass('select2-container--' + this.options.get('theme'));
  
      $container.data('element', this.$element);
  
      return $container;
    };
  
    return Select2;
  });
  