import { action, computed } from '@ember/object';
import { assert } from '@ember/debug';
import Component from '@glimmer/component';
import { next, schedule } from '@ember/runloop';
import { inject as service } from '@ember/service';
import transitionEnd from 'ember-bootstrap/utils/transition-end';
import { getDestinationElement } from 'ember-bootstrap/utils/dom';
import usesTransition from 'ember-bootstrap/utils/decorators/uses-transition';
import isFastBoot from 'ember-bootstrap/utils/is-fastboot';
import deprecateSubclassing from 'ember-bootstrap/utils/deprecate-subclassing';
import arg from '../utils/decorators/arg';
import { tracked } from '@glimmer/tracking';
import { ref } from 'ember-ref-bucket';

function nextRunloop() {
  return new Promise((resolve) => next(resolve));
}

function afterRender() {
  return new Promise((resolve) => schedule('afterRender', resolve));
}

/**
  Component for creating [Bootstrap modals](http://getbootstrap.com/javascript/#modals) with custom markup.

  ### Usage

  ```hbs
  <BsModal @onSubmit={{action "submit"}} as |Modal|>
    <Modal.header>
      <h4 class="modal-title"><i class="glyphicon glyphicon-alert"></i> Alert</h4>
    </Modal.header>
    <Modal.body>
      Are you absolutely sure you want to do that???
    </Modal.body>
    <Modal.footer as |footer|>
      <BsButton @onClick={{action Modal.close}} @type="danger">Oh no, forget it!</BsButton>
      <BsButton @onClick={{action Modal.submit}} @type="success">Yeah!</BsButton>
    </Modal.footer>
  </BsModal>
  ```

  The component yields references to the following contextual components, that you can use to further customize the output:

  * [modal.body](Components.ModalBody.html)
  * [modal.header](Components.ModalHeader.html)
  * [modal.footer](Components.ModalFooter.html)

  Furthermore references to the following actions are yielded:

  * `close`: triggers the `onHide` action and closes the modal
  * `submit`: triggers the `onSubmit` action (or the submit event on a form if present in the body element)

  ### Further reading

  See the documentation of the [bs-modal-simple](Components.ModalSimple.html) component for further examples.

  *Note that only invoking the component in a template as shown above is considered part of its public API. Extending from it (subclassing) is generally not supported, and may break at any time.*

  @class Modal
  @namespace Components
  @extends Glimmer.Component
  @public
*/
@deprecateSubclassing
export default class Modal extends Component {
  @service('-document')
  document;

  /**
   * @property _isOpen
   * @private
   */
  _isOpen = false;

  /**
   * Set to false to disable fade animations.
   *
   * @property fade
   * @type boolean
   * @default true
   * @public
   */

  get _fade() {
    let isFB = isFastBoot(this);
    return this.args.fade === undefined ? !isFB : this.args.fade;
  }

  /**
   * Used to apply Bootstrap's visibility classes.
   *
   * @property showModal
   * @type boolean
   * @default false
   * @private
   */
  @tracked
  showModal = this.open && (!this._fade || isFastBoot(this));

  /**
   * Render modal markup?
   *
   * @property inDom
   * @type boolean
   * @default false
   * @private
   */
  @tracked
  inDom = this.open;

  /**
   * @property paddingLeft
   * @type number|undefined
   * @private
   */
  @tracked
  paddingLeft;

  /**
   * @property paddingRight
   * @type number|undefined
   * @private
   */
  @tracked
  paddingRight;

  /**
   * Visibility of the modal. Toggle to show/hide with CSS transitions.
   *
   * When the modal is closed by user interaction this property will not update by using two-way bindings in order
   * to follow DDAU best practices. If you want to react to such changes, subscribe to the `onHide` action
   *
   * @property open
   * @type boolean
   * @default true
   * @public
   */
  @arg
  open = true;

  /**
   * Use a semi-transparent modal background to hide the rest of the page.
   *
   * @property backdrop
   * @type boolean
   * @default true
   * @public
   */
  @arg
  backdrop = true;

  /**
   * @property shouldShowBackdrop
   * @type boolean
   * @private
   */
  @tracked
  shouldShowBackdrop = this.open && this.backdrop;

  /**
   * Closes the modal when escape key is pressed.
   *
   * @property keyboard
   * @type boolean
   * @default true
   * @public
   */
  @arg
  keyboard = true;

  /**
   * [BS4 only!] Vertical position, either 'top' (default) or 'center'
   * 'center' will apply the `modal-dialog-centered` class
   *
   * @property position
   * @type {string}
   * @default 'top'
   * @public
   */
  @arg
  position = 'top';

  /**
   * [BS4 only!] Allows scrolling within the modal body
   * 'true' will apply the `modal-dialog-scrollable` class
   *
   * @property scrollable
   * @type boolean
   * @default false
   * @public
   */
  @arg
  scrollable = false;

  /**
   *  [BS5 only!] Allows adding fullscreen mode for modals. It will
   *  apply the `modal-fullscreen` class when using `true` and
   *  `modal-fullscreen-[x]-down` class when using BS breakpoints
   *   ([x] = `sm`, `md`, `lg`, `xl`, `xxl`).
   *
   * Also see the [Bootstrap docs](https://getbootstrap.com/docs/5.1/components/modal/#fullscreen-modal)
   *
   * @property fullscreen
   * @type {(Boolean|String)}
   * @default false
   * @public
   */

  /**
   * @property dialogComponent
   * @type {String}
   * @private
   */

  /**
   * @property headerComponent
   * @type {String}
   * @private
   */

  /**
   * @property bodyComponent
   * @type {String}
   * @private
   */

  /**
   * @property footerComponent
   * @type {String}
   * @private
   */

  /**
   * Property for size styling, set to null (default), 'lg' or 'sm'
   *
   * Also see the [Bootstrap docs](http://getbootstrap.com/javascript/#modals-sizes)
   *
   * @property size
   * @type String
   * @public
   */

  /**
   * If true clicking on the backdrop will close the modal.
   *
   * @property backdropClose
   * @type boolean
   * @default true
   * @public
   */
  @arg
  backdropClose = true;

  /**
   * If true component will render in place, rather than be wormholed.
   *
   * @property renderInPlace
   * @type boolean
   * @default false
   * @public
   */
  @arg
  renderInPlace = false;

  /**
   * @property _renderInPlace
   * @type boolean
   * @private
   */
  get _renderInPlace() {
    return this.renderInPlace || !this.destinationElement;
  }

  /**
   * The duration of the fade transition
   *
   * @property transitionDuration
   * @type number
   * @default 300
   * @public
   */
  @arg
  transitionDuration = 300;

  /**
   * The duration of the backdrop fade transition
   *
   * @property backdropTransitionDuration
   * @type number
   * @default 150
   * @public
   */
  @arg
  backdropTransitionDuration = 150;

  /**
   * Use CSS transitions?
   *
   * @property usesTransition
   * @type boolean
   * @readonly
   * @private
   */
  @usesTransition('_fade')
  usesTransition;

  destinationElement = getDestinationElement(this);

  /**
   * The DOM element of the `.modal` element.
   *
   * @property modalElement
   * @type HTMLElement
   * @readonly
   * @private
   */
  @ref('modalElement') modalElement;

  /**
   * The DOM element of the backdrop element.
   *
   * @property backdropElement
   * @type HTMLElement
   * @readonly
   * @private
   */
  @ref('backdropElement') backdropElement;

  /**
   * @type boolean
   * @readonly
   * @private
   */
  isFastBoot = isFastBoot(this);

  /**
   * The action to be sent when the modal footer's submit button (if present) is pressed.
   * Note that if your modal body contains a form (e.g. [Components.Form](Components.Form.html)) this action will
   * not be triggered. Instead, a submit event will be triggered on the form itself. See the class description for an
   * example.
   *
   * @property onSubmit
   * @type function
   * @public
   */

  /**
   * The action to be sent when the modal is closing.
   * This will be triggered by pressing the modal header's close button (x button) or the modal footer's close button.
   * Note that this will happen before the modal is hidden from the DOM, as the fade transitions will still need some
   * time to finish. Use the `onHidden` if you need the modal to be hidden when the action triggers.
   *
   * You can return false to prevent closing the modal automatically, and do that in your action by
   * setting `open` to false.
   *
   * @property onHide
   * @type function
   * @public
   */

  /**
   * The action to be sent after the modal has been completely hidden (including the CSS transition).
   *
   * @property onHidden
   * @type function
   * @default null
   * @public
   */

  /**
   * The action to be sent when the modal is opening.
   * This will be triggered immediately after the modal is shown (so it's safe to access the DOM for
   * size calculations and the like). This means that if fade=true, it will be shown in between the
   * backdrop animation and the fade animation.
   *
   * @property onShow
   * @type function
   * @default null
   * @public
   */

  /**
   * The action to be sent after the modal has been completely shown (including the CSS transition).
   *
   * @property onShown
   * @type function
   * @public
   */

  @action
  close() {
    if (this.args.onHide?.() !== false) {
      this.hide();
    }
  }

  @action
  doSubmit() {
    let forms = this.modalElement.querySelectorAll('.modal-body form');
    if (forms.length > 0) {
      // trigger submit event on body forms
      let event = document.createEvent('Events');
      event.initEvent('submit', true, true);
      Array.prototype.slice.call(forms).forEach((form) => form.dispatchEvent(event));
    } else {
      // if we have no form, we send a submit action
      this.args.onSubmit?.();
    }
  }

  /**
   * Show the modal
   *
   * @method show
   * @private
   */
  async show() {
    if (this._isOpen) {
      return;
    }
    this._isOpen = true;

    this.addBodyClass();

    this.inDom = true;

    await this.showBackdrop();

    if (this.isDestroyed) {
      return;
    }

    if (!isFastBoot(this)) {
      this.checkScrollbar();
      this.setScrollbar();
    }

    await afterRender();

    const { modalElement } = this;
    if (!modalElement) {
      return;
    }

    if (!isFastBoot(this)) {
      modalElement.scrollTop = 0;
      this.adjustDialog();
    }

    this.showModal = true;
    this.args.onShow?.();

    if (this.usesTransition) {
      await transitionEnd(modalElement, this.transitionDuration);
    }

    this.args.onShown?.();
  }

  /**
   * Hide the modal
   *
   * @method hide
   * @private
   */
  async hide() {
    if (!this._isOpen) {
      return;
    }
    this._isOpen = false;

    this.showModal = false;

    if (this.usesTransition) {
      await transitionEnd(this.modalElement, this.transitionDuration);
    }

    await this.hideModal();
  }

  /**
   * Clean up after modal is hidden and call onHidden
   *
   * @method hideModal
   * @private
   */
  async hideModal() {
    if (this.isDestroyed) {
      return;
    }

    await this.hideBackdrop();

    this.removeBodyClass();

    if (!isFastBoot(this)) {
      this.resetAdjustments();
      this.resetScrollbar();
    }

    this.inDom = false;
    this.args.onHidden?.();
  }

  /**
   * Show the backdrop
   *
   * @method showBackdrop
   * @async
   * @private
   */
  async showBackdrop() {
    if (!this.backdrop || !this.usesTransition) {
      return;
    }

    this.shouldShowBackdrop = true;

    await nextRunloop();

    const { backdropElement } = this;
    assert('Backdrop element should be in DOM', backdropElement);

    await transitionEnd(backdropElement, this.backdropTransitionDuration);
  }

  /**
   * Hide the backdrop
   *
   * @method hideBackdrop
   * @async
   * @private
   */
  async hideBackdrop() {
    if (!this.backdrop) {
      return;
    }

    if (this.usesTransition) {
      const { backdropElement } = this;
      assert('Backdrop element should be in DOM', backdropElement);

      await transitionEnd(backdropElement, this.backdropTransitionDuration);
    }

    if (this.isDestroyed) {
      return;
    }

    this.shouldShowBackdrop = false;
  }

  /**
   * @method adjustDialog
   * @private
   */
  @action
  adjustDialog() {
    let modalIsOverflowing = this.modalElement.scrollHeight > document.documentElement.clientHeight;
    this.paddingLeft = !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : undefined;
    this.paddingRight = this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : undefined;
  }

  /**
   * @method resetAdjustments
   * @private
   */
  resetAdjustments() {
    this.paddingLeft = undefined;
    this.paddingRight = undefined;
  }

  /**
   * @method checkScrollbar
   * @private
   */
  checkScrollbar() {
    const fullWindowWidth = window.innerWidth;
    this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth;
  }

  /**
   * @method setScrollbar
   * @private
   */
  setScrollbar() {
    let bodyPad = parseInt(document.body.style.paddingRight || 0, 10);
    this._originalBodyPad = document.body.style.paddingRight || '';
    if (this.bodyIsOverflowing) {
      document.body.style.paddingRight = bodyPad + this.scrollbarWidth;
    }
  }

  /**
   * @method resetScrollbar
   * @private
   */
  resetScrollbar() {
    document.body.style.paddingRight = this._originalBodyPad;
  }

  addBodyClass() {
    // special handling for FastBoot, where real `document` is not available
    if (isFastBoot(this)) {
      // a SimpleDOM instance with just a subset of the DOM API!
      let document = this.document;

      let existingClasses = document.body.getAttribute('class') || '';
      if (!existingClasses.includes('modal-open')) {
        document.body.setAttribute('class', `modal-open ${existingClasses}`);
      }
    } else {
      document.body.classList.add('modal-open');
    }
  }

  removeBodyClass() {
    if (isFastBoot(this)) {
      // no need for FastBoot support here
      return;
    }

    document.body.classList.remove('modal-open');
  }

  /**
   * @property scrollbarWidth
   * @type number
   * @readonly
   * @private
   */
  @computed('modalElement')
  get scrollbarWidth() {
    let scrollDiv = document.createElement('div');
    scrollDiv.className = 'modal-scrollbar-measure';
    let modalEl = this.modalElement;
    modalEl.parentNode.insertBefore(scrollDiv, modalEl.nextSibling);
    let scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
    scrollDiv.parentNode.removeChild(scrollDiv);
    return scrollbarWidth;
  }

  willDestroy() {
    super.willDestroy(...arguments);

    this.removeBodyClass();

    if (!isFastBoot(this)) {
      this.resetScrollbar();
    }
  }

  @action
  handleVisibilityChanges() {
    if (this.open) {
      this.show();
    } else {
      this.hide();
    }
  }
}