/**
 * @file
 * Bootstrap Modals.
 *
 * @param {jQuery} $
 * @param {Drupal} Drupal
 * @param {Drupal.bootstrap} Bootstrap
 * @param {Attributes} Attributes
 * @param {drupalSettings} drupalSettings
 */
(function ($, Drupal, Bootstrap, Attributes, drupalSettings) {
  'use strict';

  /**
   * Only process this once.
   */
  Bootstrap.once('modal.jquery.ui.bridge', function (settings) {
    // RTL support.
    var rtl = document.documentElement.getAttribute('dir').toLowerCase() === 'rtl';

    // Override drupal.dialog button classes. This must be done on DOM ready
    // since core/drupal.dialog technically depends on this file and has not
    // yet set their default settings.
    $(function () {
      drupalSettings.dialog.buttonClass = 'btn';
      drupalSettings.dialog.buttonPrimaryClass = 'btn-primary';
    });

    // Create the "dialog" plugin bridge.
    Bootstrap.Dialog.Bridge = function (options) {
      var args = Array.prototype.slice.call(arguments);
      var $element = $(this);
      var type = options && options.dialogType || $element[0].dialogType || 'modal';

      $element[0].dialogType = type;

      var handler = Bootstrap.Dialog.Handler.get(type);

      // When only options are passed, jQuery UI dialog treats this like a
      // initialization method. Destroy any existing Bootstrap modal and
      // recreate it using the contents of the dialog HTML.
      if (args.length === 1 && typeof options === 'object') {
        this.each(function () {
          handler.ensureModalStructure(this, options);
        });

        // Proxy to the Bootstrap Modal plugin, indicating that this is a
        // jQuery UI dialog bridge.
        return handler.invoke(this, {
          dialogOptions: options,
          jQueryUiBridge: true
        });
      }

      // Otherwise, proxy all arguments to the Bootstrap Modal plugin.
      var ret;
      try {
        ret = handler.invoke.apply(handler, [this].concat(args));
      }
      catch (e) {
        Bootstrap.warn(e);
      }

      // If just one element and there was a result returned for the option passed,
      // then return the result. Otherwise, just return the jQuery object.
      return this.length === 1 && ret !== void 0 ? ret : this;
    };

    // Assign the jQuery "dialog" plugin to use to the bridge.
    Bootstrap.createPlugin('dialog', Bootstrap.Dialog.Bridge);

    // Create the "modal" plugin bridge.
    Bootstrap.Modal.Bridge = function () {
      var Modal = this;

      return {
        DEFAULTS: {
          // By default, this option is disabled. It's only flagged when a modal
          // was created using $.fn.dialog above.
          jQueryUiBridge: false
        },
        prototype: {

          /**
           * Handler for $.fn.dialog('close').
           */
          close: function () {
            var _this = this;

            this.hide.apply(this, arguments);

            // For some reason (likely due to the transition event not being
            // registered properly), the backdrop doesn't always get removed
            // after the above "hide" method is invoked . Instead, ensure the
            // backdrop is removed after the transition duration by manually
            // invoking the internal "hideModal" method shortly thereafter.
            setTimeout(function () {
              if (!_this.isShown && _this.$backdrop) {
                _this.hideModal();
              }
            }, (Modal.TRANSITION_DURATION !== void 0 ? Modal.TRANSITION_DURATION : 300) + 10);
          },

          /**
           * Creates any necessary buttons from dialog options.
           */
          createButtons: function () {
            var handler = Bootstrap.Dialog.Handler.get(this.$element);
            this.$footer.find(handler.selectors.buttons).remove();

            // jQuery UI supports both objects and arrays. Unfortunately
            // developers have misunderstood and abused this by simply placing
            // the objects that should be in an array inside an object with
            // arbitrary keys (likely to target specific buttons as a hack).
            var buttons = this.options.dialogOptions && this.options.dialogOptions.buttons || [];
            if (!Array.isArray(buttons)) {
              var array = [];
              for (var k in buttons) {
                // Support the proper object values: label => click callback.
                if (typeof buttons[k] === 'function') {
                  array.push({
                    label: k,
                    click: buttons[k],
                  });
                }
                // Support nested objects, but log a warning.
                else if (buttons[k].text || buttons[k].label) {
                  Bootstrap.warn('Malformed jQuery UI dialog button: @key. The button object should be inside an array.', {
                    '@key': k
                  });
                  array.push(buttons[k]);
                }
                else {
                  Bootstrap.unsupported('button', k, buttons[k]);
                }
              }
              buttons = array;
            }

            if (buttons.length) {
              var $buttons = $('<div class="modal-buttons"/>').appendTo(this.$footer);
              for (var i = 0, l = buttons.length; i < l; i++) {
                var button = buttons[i];
                var $button = $(Drupal.theme('bootstrapModalDialogButton', button));

                // Invoke the "create" method for jQuery UI buttons.
                if (typeof button.create === 'function') {
                  button.create.call($button[0]);
                }

                // Bind the "click" method for jQuery UI buttons to the modal.
                if (typeof button.click === 'function') {
                  $button.on('click', button.click.bind(this.$element));
                }

                $buttons.append($button);
              }
            }

            // Toggle footer visibility based on whether it has child elements.
            this.$footer[this.$footer.children()[0] ? 'show' : 'hide']();
          },

          /**
           * Initializes the Bootstrap Modal.
           */
          init: function () {
            var handler = Bootstrap.Dialog.Handler.get(this.$element);
            if (!this.$dialog) {
              this.$dialog = this.$element.find(handler.selectors.dialog);
            }
            this.$dialog.addClass('js-drupal-dialog');

            if (!this.$header) {
              this.$header = this.$dialog.find(handler.selectors.header);
            }
            if (!this.$title) {
              this.$title = this.$dialog.find(handler.selectors.title);
            }
            if (!this.$close) {
              this.$close = this.$header.find(handler.selectors.close);
            }
            if (!this.$footer) {
              this.$footer = this.$dialog.find(handler.selectors.footer);
            }
            if (!this.$content) {
              this.$content = this.$dialog.find(handler.selectors.content);
            }
            if (!this.$dialogBody) {
              this.$dialogBody = this.$dialog.find(handler.selectors.body);
            }

            // Relay necessary events.
            if (this.options.jQueryUiBridge) {
              this.$element.on('hide.bs.modal',   Bootstrap.relayEvent(this.$element, 'dialogbeforeclose', false));
              this.$element.on('hidden.bs.modal', Bootstrap.relayEvent(this.$element, 'dialogclose', false));
              this.$element.on('show.bs.modal',   Bootstrap.relayEvent(this.$element, 'dialogcreate', false));
              this.$element.on('shown.bs.modal',  Bootstrap.relayEvent(this.$element, 'dialogopen', false));
            }

            // Create a footer if one doesn't exist.
            // This is necessary in case dialog.ajax.js decides to add buttons.
            if (!this.$footer[0]) {
              this.$footer = handler.theme('footer', {}, true).insertAfter(this.$dialogBody);
            }

            // Map the initial options.
            $.extend(true, this.options, this.mapDialogOptions(this.options));

            // Update buttons.
            this.createButtons();

            // Now call the parent init method.
            this.super();

            // Handle autoResize option (this is a drupal.dialog option).
            if (this.options.dialogOptions && this.options.dialogOptions.autoResize && this.options.dialogOptions.position) {
              this.position(this.options.dialogOptions.position);
            }

            // If show is enabled and currently not shown, show it.
            if (this.options.jQueryUiBridge && this.options.show && !this.isShown) {
              this.show();
            }
          },

          /**
           * Handler for $.fn.dialog('instance').
           */
          instance: function () {
            Bootstrap.unsupported('method', 'instance', arguments);
          },

          /**
           * Handler for $.fn.dialog('isOpen').
           */
          isOpen: function () {
            return !!this.isShown;
          },

          /**
           * Maps dialog options to the modal.
           *
           * @param {Object} options
           *   The options to map.
           */
          mapDialogOptions: function (options) {
            // Retrieve the dialog handler for this type.
            var handler = Bootstrap.Dialog.Handler.get(this.$element);

            var mappedOptions = {};
            var dialogOptions = options.dialogOptions || {};

            // Remove any existing dialog options.
            delete options.dialogOptions;

            // Separate Bootstrap modal options from jQuery UI dialog options.
            for (var k in options) {
              if (Modal.DEFAULTS.hasOwnProperty(k)) {
                mappedOptions[k] = options[k];
              }
              else {
                dialogOptions[k] = options[k];
              }
            }


            // Handle CSS properties.
            var cssUnitRegExp = /^([+-]?(?:\d+|\d*\.\d+))([a-z]*|%)?$/;
            var parseCssUnit = function (value, defaultUnit) {
              var parts = ('' + value).match(cssUnitRegExp);
              return parts && parts[1] !== void 0 ? parts[1] + (parts[2] || defaultUnit || 'px') : null;
            };
            var styles = {};
            var cssProperties = ['height', 'maxHeight', 'maxWidth', 'minHeight', 'minWidth', 'width'];
            for (var i = 0, l = cssProperties.length; i < l; i++) {
              var prop = cssProperties[i];
              if (dialogOptions[prop] !== void 0) {
                var value = parseCssUnit(dialogOptions[prop]);
                if (value) {
                  styles[prop] = value;

                  // If there's a defined height of some kind, enforce the modal
                  // to use flex (on modern browsers). This will ensure that
                  // the core autoResize calculations don't cause the content
                  // to overflow.
                  if (dialogOptions.autoResize && (prop === 'height' || prop === 'maxHeight')) {
                    styles.display = 'flex';
                    styles.flexDirection = 'column';
                    this.$dialogBody.css('overflow', 'scroll');
                  }
                }
              }
            }

            // Apply mapped CSS styles to the modal-content container.
            this.$content.css(styles);

            // Handle deprecated "dialogClass" option by merging it with "classes".
            var classesMap = {
              'ui-dialog': 'modal-content',
              'ui-dialog-titlebar': 'modal-header',
              'ui-dialog-title': 'modal-title',
              'ui-dialog-titlebar-close': 'close',
              'ui-dialog-content': 'modal-body',
              'ui-dialog-buttonpane': 'modal-footer'
            };
            if (dialogOptions.dialogClass) {
              if (dialogOptions.classes === void 0) {
                dialogOptions.classes = {};
              }
              if (dialogOptions.classes['ui-dialog'] === void 0) {
                dialogOptions.classes['ui-dialog'] = '';
              }
              var dialogClass = dialogOptions.classes['ui-dialog'].split(' ');
              dialogClass.push(dialogOptions.dialogClass);
              dialogOptions.classes['ui-dialog'] = dialogClass.join(' ');
              delete dialogOptions.dialogClass;
            }

            // Add jQuery UI classes to elements in case developers target them
            // in callbacks.
            for (k in classesMap) {
              this.$element.find('.' + classesMap[k]).addClass(k);
            }

            // Bind events.
            var events = [
              'beforeClose', 'close',
              'create',
              'drag', 'dragStart', 'dragStop',
              'focus',
              'open',
              'resize', 'resizeStart', 'resizeStop'
            ];
            for (i = 0, l = events.length; i < l; i++) {
              var event = events[i].toLowerCase();
              if (dialogOptions[event] === void 0 || typeof dialogOptions[event] !== 'function') continue;
              this.$element.on('dialog' + event, dialogOptions[event]);
            }

            // Support title attribute on the modal.
            var title;
            if ((dialogOptions.title === null || dialogOptions.title === void 0) && (title = this.$element.attr('title'))) {
              dialogOptions.title = title;
            }

            // Handle the reset of the options.
            for (var name in dialogOptions) {
              if (!dialogOptions.hasOwnProperty(name) || dialogOptions[name] === void 0) continue;

              switch (name) {
                case 'appendTo':
                  Bootstrap.unsupported('option', name, dialogOptions.appendTo);
                  break;

                case 'autoOpen':
                  mappedOptions.show = dialogOptions.show = !!dialogOptions.autoOpen;
                  break;

                case 'classes':
                  if (dialogOptions.classes) {
                    for (var key in dialogOptions.classes) {
                      if (dialogOptions.classes.hasOwnProperty(key) && classesMap[key] !== void 0) {
                        // Run through Attributes to sanitize classes.
                        var attributes = Attributes.create().addClass(dialogOptions.classes[key]).toPlainObject();
                        var selector = '.' + classesMap[key];
                        this.$element.find(selector).addClass(attributes['class']);
                      }
                    }
                  }
                  break;

                case 'closeOnEscape':
                  mappedOptions.keyboard = !!dialogOptions.closeOnEscape;
                  if (!dialogOptions.closeOnEscape && dialogOptions.modal) {
                    mappedOptions.backdrop = 'static';
                  }
                  break;

                case 'closeText':
                  Bootstrap.unsupported('option', name, dialogOptions.closeText);
                  break;

                case 'draggable':
                  this.$content
                    .draggable({
                      handle: handler.selectors.header,
                      drag: Bootstrap.relayEvent(this.$element, 'dialogdrag'),
                      start: Bootstrap.relayEvent(this.$element, 'dialogdragstart'),
                      end: Bootstrap.relayEvent(this.$element, 'dialogdragend')
                    })
                    .draggable(dialogOptions.draggable ? 'enable' : 'disable');
                  break;

                case 'hide':
                  if (dialogOptions.hide === false || dialogOptions.hide === true) {
                    this.$element[dialogOptions.hide ? 'addClass' : 'removeClass']('fade');
                    mappedOptions.animation = dialogOptions.hide;
                  }
                  else {
                    Bootstrap.unsupported('option', name + ' (complex animation)', dialogOptions.hide);
                  }
                  break;

                case 'modal':
                  if (!dialogOptions.closeOnEscape && dialogOptions.modal) {
                    mappedOptions.backdrop = 'static';
                  }
                  else {
                    mappedOptions.backdrop = dialogOptions.modal;
                  }

                  // If not a modal and no initial position, center it.
                  if (!dialogOptions.modal && !dialogOptions.position) {
                    this.position({ my: 'center', of: window });
                  }
                  break;

                case 'position':
                  this.position(dialogOptions.position);
                  break;

                // Resizable support (must initialize first).
                case 'resizable':
                  this.$content
                    .resizable({
                      resize: Bootstrap.relayEvent(this.$element, 'dialogresize'),
                      start: Bootstrap.relayEvent(this.$element, 'dialogresizestart'),
                      end: Bootstrap.relayEvent(this.$element, 'dialogresizeend')
                    })
                    .resizable(dialogOptions.resizable ? 'enable' : 'disable');
                  break;

                case 'show':
                  if (dialogOptions.show === false || dialogOptions.show === true) {
                    this.$element[dialogOptions.show ? 'addClass' : 'removeClass']('fade');
                    mappedOptions.animation = dialogOptions.show;
                  }
                  else {
                    Bootstrap.unsupported('option', name + ' (complex animation)', dialogOptions.show);
                  }
                  break;

                case 'title':
                  this.$title.text(dialogOptions.title);
                  break;

              }
            }

            // Add the supported dialog options to the mapped options.
            mappedOptions.dialogOptions = dialogOptions;

            return mappedOptions;
          },

          /**
           * Handler for $.fn.dialog('moveToTop').
           */
          moveToTop: function () {
            Bootstrap.unsupported('method', 'moveToTop', arguments);
          },

          /**
           * Handler for $.fn.dialog('option').
           */
          option: function () {
            var clone = {options: {}};

            // Apply the parent option method to the clone of current options.
            this.super.apply(clone, arguments);

            // Merge in the cloned mapped options.
            $.extend(true, this.options, this.mapDialogOptions(clone.options));

            // Update buttons.
            this.createButtons();
          },

          position: function(position) {
            // Reset modal styling.
            this.$element.css({
              bottom: 'initial',
              overflow: 'visible',
              right: 'initial'
            });

            // Position the modal.
            this.$element.position(position);
          },

          /**
           * Handler for $.fn.dialog('open').
           */
          open: function () {
            this.show.apply(this, arguments);
          },

          /**
           * Handler for $.fn.dialog('widget').
           */
          widget: function () {
            return this.$element;
          }
        }
      };
    };

    // Extend the Bootstrap Modal plugin constructor class.
    Bootstrap.extendPlugin('modal', Bootstrap.Modal.Bridge);

    // Register default core dialog type handlers.
    Bootstrap.Dialog.Handler.register('dialog');
    Bootstrap.Dialog.Handler.register('modal');

    /**
     * Extend Drupal theming functions.
     */
    $.extend(Drupal.theme, /** @lend Drupal.theme */ {

      /**
       * Renders a jQuery UI Dialog compatible button element.
       *
       * @param {Object} button
       *   The button object passed in the dialog options.
       *
       * @return {String}
       *   The modal dialog button markup.
       *
       * @see http://api.jqueryui.com/dialog/#option-buttons
       * @see http://api.jqueryui.com/button/
       */
      bootstrapModalDialogButton: function (button) {
        var attributes = Attributes.create();

        var icon = '';
        var iconPosition = button.iconPosition || 'beginning';
        iconPosition = (iconPosition === 'end' && !rtl) || (iconPosition === 'beginning' && rtl) ? 'after' : 'before';

        // Handle Bootstrap icons differently.
        if (button.bootstrapIcon) {
          icon = Drupal.theme('icon', 'bootstrap', button.icon);
        }
        // Otherwise, assume it's a jQuery UI icon.
        // @todo Map jQuery UI icons to Bootstrap icons?
        else if (button.icon) {
          var iconAttributes = Attributes.create()
            .addClass(['ui-icon', button.icon])
            .set('aria-hidden', 'true');
          icon = '<span' + iconAttributes + '></span>';
        }

        // Label. Note: jQuery UI dialog has an inconsistency where it uses
        // "text" instead of "label", so both need to be supported.
        var value = button.label || button.text;

        // Show/hide label.
        if (icon && ((button.showLabel !== void 0 && !button.showLabel) || (button.text !== void 0 && !button.text))) {
          value = '<span' + Attributes.create().addClass('sr-only') + '>' + value + '</span>';
        }
        attributes.set('value', iconPosition === 'before' ? icon + value : value + icon);

        // Handle disabled.
        attributes[button.disabled ? 'set' :'remove']('disabled', 'disabled');

        if (button.classes) {
          attributes.addClass(Object.keys(button.classes).map(function(key) { return button.classes[key]; }));
        }
        if (button['class']) {
          attributes.addClass(button['class']);
        }
        if (button.primary) {
          attributes.addClass('btn-primary');
        }

        return Drupal.theme('button', attributes);
      }

    });

  });


})(window.jQuery, window.Drupal, window.Drupal.bootstrap, window.Attributes, window.drupalSettings);
