define([

  'jquery',

  './options',

  './utils',

  './keys'

], function ($, Options, Utils, KEYS) {

  var Select2 = function ($element, options) {

    if (Utils.GetData($element[0], 'select2') != null) {

      Utils.GetData($element[0], '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;

    Utils.StoreData($element[0], '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();



    Utils.StoreData($element[0], 'select2', this);



    // Ensure backwards compatibility with $element.data('select2').

    $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;

    }



    if (method == 'computedstyle') {

      var computedStyle = window.getComputedStyle($element[0]);



      return computedStyle.width;

    }



    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.$element.on('focus.select2', function (evt) {

      self.trigger('focus', evt);

    });



    this._syncA = Utils.bind(this._syncAttributes, this);

    this._syncS = Utils.bind(this._syncSubtree, this);



    if (this.$element[0].attachEvent) {

      this.$element[0].attachEvent('onpropertychange', this._syncA);

    }



    var observer = window.MutationObserver ||

      window.WebKitMutationObserver ||

      window.MozMutationObserver

    ;



    if (observer != null) {

      this._observer = new observer(function (mutations) {

        $.each(mutations, self._syncA);

        $.each(mutations, self._syncS);

      });

      this._observer.observe(this.$element[0], {

        attributes: true,

        childList: true,

        subtree: false

      });

    } else if (this.$element[0].addEventListener) {

      this.$element[0].addEventListener(

        'DOMAttrModified',

        self._syncA,

        false

      );

      this.$element[0].addEventListener(

        'DOMNodeInserted',

        self._syncS,

        false

      );

      this.$element[0].addEventListener(

        'DOMNodeRemoved',

        self._syncS,

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

    }

  };



  Select2.prototype._syncSubtree = function (evt, mutations) {

    var changed = false;

    var self = this;



    // Ignore any mutation events raised for elements that aren't options or

    // optgroups. This handles the case when the select element is destroyed

    if (

      evt && evt.target && (

        evt.target.nodeName !== 'OPTION' && evt.target.nodeName !== 'OPTGROUP'

      )

    ) {

      return;

    }



    if (!mutations) {

      // If mutation events aren't supported, then we can only assume that the

      // change affected the selections

      changed = true;

    } else if (mutations.addedNodes && mutations.addedNodes.length > 0) {

      for (var n = 0; n < mutations.addedNodes.length; n++) {

        var node = mutations.addedNodes[n];



        if (node.selected) {

          changed = true;

        }

      }

    } else if (mutations.removedNodes && mutations.removedNodes.length > 0) {

      changed = true;

    }



    // Only re-pull the data if we think there is a change

    if (changed) {

      this.dataAdapter.current(function (currentData) {

        self.trigger('selection:update', {

          data: currentData

        });

      });

    }

  };



  /**

   * 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',

      'clear': 'clearing'

    };



    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._syncA);

    }



    if (this._observer != null) {

      this._observer.disconnect();

      this._observer = null;

    } else if (this.$element[0].removeEventListener) {

      this.$element[0]

        .removeEventListener('DOMAttrModified', this._syncA, false);

      this.$element[0]

        .removeEventListener('DOMNodeInserted', this._syncS, false);

      this.$element[0]

        .removeEventListener('DOMNodeRemoved', this._syncS, false);

    }



    this._syncA = null;

    this._syncS = null;



    this.$element.off('.select2');

    this.$element.attr('tabindex',

    Utils.GetData(this.$element[0], 'old-tabindex'));



    this.$element.removeClass('select2-hidden-accessible');

    this.$element.attr('aria-hidden', 'false');

    Utils.RemoveData(this.$element[0]);

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



    Utils.StoreData($container[0], 'element', this.$element);



    return $container;

  };



  return Select2;

});

