;(function ($, window, document, undefined) {
  var pluginName = 'bootstrapDropdownHover',
      defaults = {
        clickBehavior: 'sticky',  // Click behavior setting:
                                  // 'default' means that the dropdown toggles on hover and on click too
                                  // 'disable' disables dropdown toggling with clicking when mouse is detected
                                  //  'sticky' means if we click on an opened dropdown then it will not hide on
                                  //           mouseleave but on a second click only
        hideTimeout: 200
      },
      _hideTimeoutHandler,
      _hardOpened = false,
      _touchstartDetected = false,
      _mouseDetected = false;

  // The actual plugin constructor
  function BootstrapDropdownHover(element, options) {
    this.element = $(element);
    this.settings = $.extend({}, defaults, options);
    this._defaults = defaults;
    this._name = pluginName;
    this.init();
  }

  function bindEvents(dropdown) {
    $('body').one('touchstart.dropdownhover', function() {
      _touchstartDetected = true;
    });

    $('body').one('mouseenter.dropdownhover', function() {
      // touchstart fires before mouseenter on touch devices
      if (!_touchstartDetected) {
        _mouseDetected = true;
      }
    });

    $('.dropdown-toggle, .dropdown-menu', dropdown.element.parent()).on('mouseenter.dropdownhover', function () {
      // seems to be a touch device
      if(_mouseDetected && !$(this).is(':hover')) {
        _mouseDetected = false;
      }

      if (!_mouseDetected) {
        return;
      }

      clearTimeout(_hideTimeoutHandler);
      if (!dropdown.element.parent().hasClass('open')) {
        _hardOpened = false;
        dropdown.element.dropdown('toggle');
      }
    });

    $('.dropdown-toggle, .dropdown-menu', dropdown.element.parent()).on('mouseleave.dropdownhover', function () {
      if (!_mouseDetected) {
        return;
      }

      if (_hardOpened) {
        return;
      }
      _hideTimeoutHandler = setTimeout(function () {
        if (dropdown.element.parent().hasClass('open')) {
          dropdown.element.dropdown('toggle');
        }
      }, dropdown.settings.hideTimeout);
    });

    dropdown.element.on('click.dropdownhover', function (e) {
      if (!_mouseDetected) {
        return;
      }

      switch(dropdown.settings.clickBehavior) {
        case 'default':
          return;
        case 'disable':
          e.preventDefault();
          e.stopImmediatePropagation();
          break;
        case 'sticky':
          if (_hardOpened) {
            _hardOpened = false;
          }
          else {
            _hardOpened = true;
            if (dropdown.element.parent().hasClass('open')) {
              e.stopImmediatePropagation();
              e.preventDefault();
            }
          }
          return;
      }
    });
  }

  function removeEvents(dropdown) {
    $('.dropdown-toggle, .dropdown-menu', dropdown.element.parent()).off('.dropdownhover');
    // seems that bootstrap binds the click handler twice after we reinitializing the plugin after a destroy...
    $('.dropdown-toggle, .dropdown-menu', dropdown.element.parent()).off('.dropdown');
    dropdown.element.off('.dropdownhover');
    $('body').off('.dropdownhover');
  }

  BootstrapDropdownHover.prototype = {
    init: function () {
      this.setClickBehavior(this.settings.clickBehavior);
      this.setHideTimeout(this.settings.hideTimeout);
      bindEvents(this);
      return this.element;
    },
    setClickBehavior: function(value) {
      this.settings.clickBehavior = value;
      return this.element;
    },
    setHideTimeout: function(value) {
      this.settings.hideTimeout = value;
      return this.element;
    },
    destroy: function() {
      clearTimeout(_hideTimeoutHandler);
      removeEvents(this);
      this.element.data('plugin_' + pluginName, null);
      return this.element;
    }
  };

  // A really lightweight plugin wrapper around the constructor,
  // preventing against multiple instantiations
  $.fn[pluginName] = function (options) {
    var args = arguments;

    // Is the first parameter an object (options), or was omitted, instantiate a new instance of the plugin.
    if (options === undefined || typeof options === 'object') {
      // This allows the plugin to be called with $.fn.bootstrapDropdownHover();
      if (!$.contains(document, $(this)[0])) {
        $('[data-toggle="dropdown"]').each(function (index, item) {
          // For each nested select, instantiate the plugin
          $(item).bootstrapDropdownHover(options);
        });
      }
      return this.each(function () {
        // If this is not a select
        if (!$(this).hasClass('dropdown-toggle') || $(this).data('toggle') !== 'dropdown') {
          $('[data-toggle="dropdown"]', this).each(function (index, item) {
            // For each nested select, instantiate the plugin
            $(item).bootstrapDropdownHover(options);
          });
        } else if (!$.data(this, 'plugin_' + pluginName)) {
          // Only allow the plugin to be instantiated once so we check that the element has no plugin instantiation yet

          // if it has no instance, create a new one, pass options to our plugin constructor,
          // and store the plugin instance in the elements jQuery data object.
          $.data(this, 'plugin_' + pluginName, new BootstrapDropdownHover(this, options));
        }
      });

      // If the first parameter is a string and it doesn't start with an underscore or "contains" the `init`-function,
      // treat this as a call to a public method.
    } else if (typeof options === 'string' && options[0] !== '_' && options !== 'init') {

      // Cache the method call to make it possible to return a value
      var returns;

      this.each(function () {
        var instance = $.data(this, 'plugin_' + pluginName);
        // Tests that there's already a plugin-instance and checks that the requested public method exists
        if (instance instanceof BootstrapDropdownHover && typeof instance[options] === 'function') {
          // Call the method of our plugin instance, and pass it the supplied arguments.
          returns = instance[options].apply(instance, Array.prototype.slice.call(args, 1));
        }
      });

      // If the earlier cached method gives a value back return the value,
      // otherwise return this to preserve chainability.
      return returns !== undefined ? returns : this;
    }

  };

})(jQuery, window, document);
