/**
 * @file
 * Drupal's off-canvas library.
 */

(($, Drupal, debounce, displace) => {
  /**
   * Off-canvas dialog implementation using jQuery Dialog.
   *
   * Transforms the regular dialogs created using Drupal.dialog when the dialog
   * element equals '#drupal-off-canvas' into a side-loading dialog.
   *
   * @namespace
   */
  Drupal.offCanvas = {
    /**
     * Storage for position information about the tray.
     *
     * @type {?String}
     */
    position: null,

    /**
     * The minimum height of the tray when opened at the top of the page.
     *
     * @type {Number}
     */
    minimumHeight: 30,

    /**
     * The minimum width to use body displace needs to match the width at which
     * the tray will be 100% width. @see core/misc/dialog/off-canvas.css
     *
     * @type {Number}
     */
    minDisplaceWidth: 768,

    /**
     * Wrapper used to position off-canvas dialog.
     *
     * @type {jQuery}
     */
    $mainCanvasWrapper: $('[data-off-canvas-main-canvas]'),

    /**
     * Determines if an element is an off-canvas dialog.
     *
     * @param {jQuery} $element
     *   The dialog element.
     *
     * @return {boolean}
     *   True this is currently an off-canvas dialog.
     */
    isOffCanvas($element) {
      return $element[0].id === 'drupal-off-canvas';
    },

    /**
     * Remove off-canvas dialog events.
     *
     * @param {jQuery} $element
     *   The target element.
     */
    removeOffCanvasEvents($element) {
      $element.off('.off-canvas');
      $(document).off('.off-canvas');
      $(window).off('.off-canvas');
    },

    /**
     * Handler fired before an off-canvas dialog has been opened.
     *
     * @param {Object} settings
     *   Settings related to the composition of the dialog.
     *
     * @return {undefined}
     */
    beforeCreate({ settings, $element }) {
      // Clean up previous dialog event handlers.
      Drupal.offCanvas.removeOffCanvasEvents($element);

      $('body').addClass('js-off-canvas-dialog-open');
      // @see http://api.jqueryui.com/position/
      settings.position = {
        my: 'left top',
        at: `${Drupal.offCanvas.getEdge()} top`,
        of: window,
      };

      /**
       * Applies initial height and with to dialog based depending on position.
       * @see http://api.jqueryui.com/dialog for all dialog options.
       */
      const position = settings.drupalOffCanvasPosition;
      const height = position === 'side' ? $(window).height() : settings.height;
      const width = position === 'side' ? settings.width : '100%';
      settings.height = height;
      settings.width = width;
    },

    /**
     * Handler fired after an off-canvas dialog has been closed.
     *
     * @return {undefined}
     */
    beforeClose({ $element }) {
      $('body').removeClass('js-off-canvas-dialog-open');
      // Remove all *.off-canvas events
      Drupal.offCanvas.removeOffCanvasEvents($element);
      Drupal.offCanvas.resetPadding();
    },

    /**
     * Handler fired when an off-canvas dialog has been opened.
     *
     * @param {jQuery} $element
     *   The off-canvas dialog element.
     * @param {Object} settings
     *   Settings related to the composition of the dialog.
     *
     * @return {undefined}
     */
    afterCreate({ $element, settings }) {
      const eventData = { settings, $element, offCanvasDialog: this };

      $element
        .on(
          'dialogContentResize.off-canvas',
          eventData,
          Drupal.offCanvas.handleDialogResize,
        )
        .on(
          'dialogContentResize.off-canvas',
          eventData,
          Drupal.offCanvas.bodyPadding,
        );

      Drupal.offCanvas
        .getContainer($element)
        .attr(`data-offset-${Drupal.offCanvas.getEdge()}`, '');

      $(window)
        .on(
          'resize.off-canvas',
          eventData,
          debounce(Drupal.offCanvas.resetSize, 100, true),
        )
        .trigger('resize.off-canvas');
    },

    /**
     * Toggle classes based on title existence.
     * Called with Drupal.offCanvas.afterCreate.
     *
     * @param {Object} settings
     *   Settings related to the composition of the dialog.
     *
     * @return {undefined}
     */
    render({ settings }) {
      $(
        '.ui-dialog-off-canvas, .ui-dialog-off-canvas .ui-dialog-titlebar',
      ).toggleClass('ui-dialog-empty-title', !settings.title);
      $('.ui-dialog-off-canvas').attr('id', 'drupal-off-canvas-wrapper');
    },

    /**
     * Adjusts the dialog on resize.
     *
     * @param {jQuery.Event} event
     *   The event triggered.
     * @param {object} event.data
     *   Data attached to the event.
     */
    handleDialogResize(event) {
      const $element = event.data.$element;
      const $container = Drupal.offCanvas.getContainer($element);

      const $offsets = $container.find(
        '> :not(#drupal-off-canvas, .ui-resizable-handle)',
      );
      let offset = 0;

      // Let scroll element take all the height available.
      $element[0].style.height = 'auto';
      const modalHeight = $container.height();

      $offsets.each((i, e) => {
        offset += $(e).outerHeight();
      });

      // Take internal padding into account.
      const scrollOffset = $element.outerHeight() - $element.height();
      $element.height(modalHeight - offset - scrollOffset);
    },

    /**
     * Resets the size of the dialog.
     *
     * @param {jQuery.Event} event
     *   The event triggered.
     * @param {object} event.data
     *   Data attached to the event.
     */
    resetSize(event) {
      const $element = event.data.$element;
      const container = Drupal.offCanvas.getContainer($element);
      const position = event.data.settings.drupalOffCanvasPosition;

      // Only remove the `data-offset-*` attribute if the value previously
      // exists and the orientation is changing.
      if (Drupal.offCanvas.position && Drupal.offCanvas.position !== position) {
        container.removeAttr(`data-offset-${Drupal.offCanvas.position}`);
      }
      // Set a minimum height on $element
      if (position === 'top') {
        $element[0].style.minHeight = `${Drupal.offCanvas.minimumHeight}px`;
      }

      displace();

      const offsets = displace.offsets;

      const topPosition =
        position === 'side' && offsets.top !== 0 ? `+${offsets.top}` : '';
      const adjustedOptions = {
        // @see http://api.jqueryui.com/position/
        position: {
          my: `${Drupal.offCanvas.getEdge()} top`,
          at: `${Drupal.offCanvas.getEdge()} top${topPosition}`,
          of: window,
        },
      };

      const height =
        position === 'side'
          ? `${$(window).height() - (offsets.top + offsets.bottom)}px`
          : event.data.settings.height;

      Object.assign(container[0].style, {
        position: 'fixed',
        height: Number.isNaN(parseFloat(height))
          ? height
          : `${parseFloat(height)}px`,
      });

      $element.dialog('option', adjustedOptions);

      $element
        ?.get(0)
        ?.dispatchEvent(
          new CustomEvent('dialogContentResize', { bubbles: true }),
        );

      Drupal.offCanvas.position = position;
    },

    /**
     * Adjusts the body padding when the dialog is resized.
     *
     * @param {jQuery.Event} event
     *   The event triggered.
     * @param {object} event.data
     *   Data attached to the event.
     */
    bodyPadding(event) {
      const position = event.data.settings.drupalOffCanvasPosition;
      if (
        position === 'side' &&
        $('body').outerWidth() < Drupal.offCanvas.minDisplaceWidth
      ) {
        return;
      }
      Drupal.offCanvas.resetPadding();
      const $element = event.data.$element;
      const $container = Drupal.offCanvas.getContainer($element);
      const mainCanvasWrapper = Drupal.offCanvas.$mainCanvasWrapper[0];

      const width = $container.outerWidth();
      const mainCanvasPadding =
        window.getComputedStyle(mainCanvasWrapper)[
          `padding-${Drupal.offCanvas.getEdge()}`
        ];

      if (position === 'side' && width !== mainCanvasPadding) {
        mainCanvasWrapper.style[`padding-${Drupal.offCanvas.getEdge()}`] =
          `${width}px`;

        $container.attr(`data-offset-${Drupal.offCanvas.getEdge()}`, width);
        $container.attr('data-offset-top', 0);
        displace();
      }

      const height = $container.outerHeight();
      if (position === 'top') {
        mainCanvasWrapper.style.paddingTop = `${height}px`;
        $container.attr('data-offset-top', height);
        $container.attr(`data-offset-${Drupal.offCanvas.getEdge()}`, 0);
        displace();
      }
    },

    /**
     * The HTML element that surrounds the dialog.
     * @param {HTMLElement} $element
     *   The dialog element.
     *
     * @return {HTMLElement}
     *   The containing element.
     */
    getContainer($element) {
      return $element.dialog('widget');
    },

    /**
     * The edge of the screen that the dialog should appear on.
     *
     * @return {string}
     *   The edge the tray will be shown on, left or right.
     */
    getEdge() {
      return document.documentElement.dir === 'rtl' ? 'left' : 'right';
    },

    /**
     * Resets main canvas wrapper and toolbar padding / margin.
     */
    resetPadding() {
      Drupal.offCanvas.$mainCanvasWrapper[0].style[
        `padding-${Drupal.offCanvas.getEdge()}`
      ] = 0;
      Drupal.offCanvas.$mainCanvasWrapper[0].style.paddingTop = 0;
      displace();
    },
  };

  /**
   * Attaches off-canvas dialog behaviors.
   *
   * @type {Drupal~behavior}
   *
   * @prop {Drupal~behaviorAttach} attach
   *   Attaches event listeners for off-canvas dialogs.
   */
  Drupal.behaviors.offCanvasEvents = {
    attach: () => {
      if (!once('off-canvas', 'html').length) {
        return;
      }

      window.addEventListener('dialog:beforecreate', (e) => {
        const $element = $(e.target);
        if (Drupal.offCanvas.isOffCanvas($element)) {
          Drupal.offCanvas.beforeCreate({
            $element,
            settings: e.settings,
          });
        }
      });

      window.addEventListener('dialog:aftercreate', (e) => {
        const $element = $(e.target);
        if (Drupal.offCanvas.isOffCanvas($element)) {
          Drupal.offCanvas.render({
            $element,
            dialog: e.dialog,
            settings: e.settings,
          });
          Drupal.offCanvas.afterCreate({
            $element,
            settings: e.settings,
          });
        }
      });

      window.addEventListener('dialog:beforeclose', (e) => {
        const $element = $(e.target);
        if (Drupal.offCanvas.isOffCanvas($element)) {
          Drupal.offCanvas.beforeClose({
            $element,
          });
        }
      });
    },
  };
})(jQuery, Drupal, Drupal.debounce, Drupal.displace);
