/**
 * External dependencies
 */
import { computePosition, autoUpdate, flip, shift } from "@floating-ui/dom";

/**
 * Internal dependencies
 */
import {
  exposeComponent,
  BaseComponent,
  EVENTS,
  triggerEvent,
  reflow,
  isRTL,
  ScrollBarHelper,
  Backdrop,
  isElement,
} from "./dom";

/**
 * Modal class
 * Adapted from bootstrap modal
 */
class Modal extends BaseComponent {
  constructor(element, config = {}) {
    // Get this._element and this._config
    super(element, config);

    // Bail if the element is not valid
    if (!element) {
      return;
    }

    const {
      modalType,
      dataset = {},
      datasetElement,
      triggerArray,
      selectorTrigger,
    } = this._config;

    // Modal type
    this.isModal = modalType === "modal";
    this.isPopover = modalType === "popover";
    this.isOffcanvas = modalType === "offcanvas";

    // Get dataset
    this._dataset = Object.assign(
      { toggleStateShown: "shown" },
      dataset,
      datasetElement?.dataset ?? {},
    );

    const {
      modalSettings = {},
      toggleElement,
      toggleState,
      toggleStateShown,
    } = this._dataset;

    // Get modal settings
    this._modalSettings = modalSettings;

    // Get the trigger elements
    if (triggerArray) {
      this._triggerArray = triggerArray;
    } else {
      const triggerSelector =
        (toggleElement && toggleElement.trim()) || selectorTrigger;

      this._triggerArray = triggerSelector
        ? Array.from(document.querySelectorAll(triggerSelector))
        : [];
    }

    // We need it for popover
    this._triggerElement = this._triggerArray?.length
      ? this._triggerArray[0]
      : null;

    // Support hover for popover
    this.hasHover =
      this.isPopover &&
      this._modalSettings?.triggerOnHover &&
      this._triggerElement;

    // Get the initial state
    this._isOpen = toggleState === toggleStateShown;

    // Mark transitioning state to false
    this._isTransitioning = false;

    // Dialog element
    this._dialog = null;

    // Build modal markup
    this._modal = this._getModalMarkup();

    // Store the Backdrop
    this._backdrop = this._initializeBackDrop();

    // Handle vertical scroll bar
    this._scrollBar = this._initializeScrollbar();

    // Initialize
    this.init();
  }

  static get NAME() {
    return "modal";
  }

  defaultConfig() {
    return {
      modalType: "modal",
      triggerArray: null,
      dataset: null,
      datasetElement: this._element,
      rootElement: document.body,
      classModalInitialized: "bb-modal-initialized",
      classModalHeader: "bb-modal-header",
      classModalTitle: "bb-modal-title",
      classTarget: "bb-modal-body",
      classModal: "bb-modal",
      classModalDialog: "bb-modal-dialog",
      classModalContent: "bb-modal-content",
      classAnimation: "fade",
      classTrigger: "",
      classShow: "show",
      classToggledOn: "is-toggled-on",
      classToggledOff: "is-toggled-off",
      classRootOpen: "modal-open",
      classDismiss: "is-close-modal",
      classStatic: "is-modal-static",
      classAnimatedWaiting: "animate__waiting",
      classAnimating: "is-animating",
      selectorTrigger: ".js-toggle-modal",
      selectorFocus: ".is-focus-first",
      selectorText: ".toggle__text",
      instanceIndex: 0,
      cachePrefix: "bb-modal-",
    };
  }

  init() {
    // Setup classes and attributes for modal, modal content, triggers
    this._setupClasses();

    // Add toogled classes and accessibility attribute on first load
    this._addAriaAndToggledClass();

    // Add click handler
    this._attachClickHandler();

    // Add hover handler
    this._attachHoverHandler();

    // Initial state
    if (this._isOpen) {
      this._isOpen = false;
      this.show();
      this._setTransitioning(false);
    }

    // Show the modal after a delay time
    this.handleCustomState(this._dataset?.toggleState);

    // Cache on escape key handler
    this._onEscape = this._closeOnEscapeKey.bind(this);

    // Trigger init event
    triggerEvent(window, EVENTS.INIT, {
      target: this._element,
      modal: this._modal,
    });
  }

  _setupClasses() {
    const modalBlock = this._element;
    const modal = this._modal;
    const triggerArray = this._triggerArray;
    const { classTrigger, classTarget, instanceIndex } = this._config;
    const { placement = "start", popoverPlacement = "bottom" } =
      this._modalSettings;

    // Add classes to the modal
    modalBlock.classList.add(classTarget);

    // Set the ID
    const modalId = `bb-modal-${instanceIndex}`;
    modalBlock.setAttribute("id", modalId);

    if (this.isOffcanvas) {
      modal.classList.add(`placement-${placement}`);
    } else if (this.isPopover) {
      const suffix =
        popoverPlacement.startsWith("top") ||
        popoverPlacement.startsWith("bottom")
          ? "y"
          : "x";
      modal.classList.add(`placement-${suffix}`);
    }

    // Add classes and attributes to the trigger elements
    if (classTrigger && !!triggerArray?.length) {
      for (const element of triggerArray) {
        if (!element.classList.contains(classTrigger)) {
          element.classList.add(classTrigger);

          element.setAttribute("aria-haspopup", "dialog");
          element.setAttribute("aria-controls", `#${modalId}`);

          // Make the div accessibility
          if (element.tagName.toUpperCase() === "DIV" && !element?.role) {
            element.setAttribute("role", "button");
            element.setAttribute("tabindex", 0);
          }
        }
      }
    }
  }

  _attachClickHandler() {
    // click handler
    this.clickHandler = this.toggle.bind(this);
    const triggerArray = this._triggerArray;

    // Bind click event
    if (!!triggerArray?.length) {
      for (const element of triggerArray) {
        element.addEventListener("click", this.clickHandler);
      }
    }
  }

  _attachHoverHandler() {
    if (!this.hasHover) {
      return;
    }

    const trigger = this._triggerElement;
    const popover = this._modal;

    const HIDE_DELAY = 120;
    let hideTimeout;

    const show = () => {
      clearTimeout(hideTimeout);
      if (!this._isOpen) {
        this.show(true);
      }
    };

    const scheduleHide = () => {
      clearTimeout(hideTimeout);
      hideTimeout = setTimeout(() => {
        if (this._isOpen) {
          this.hide();
        }
      }, HIDE_DELAY);
    };

    const cancelHide = () => {
      clearTimeout(hideTimeout);
    };

    trigger.addEventListener("pointerenter", show);
    trigger.addEventListener("pointerleave", (e) => {
      if (popover.contains(e.relatedTarget)) return;
      scheduleHide();
    });

    popover.addEventListener("pointerenter", cancelHide);
    popover.addEventListener("pointerleave", (e) => {
      if (trigger.contains(e.relatedTarget)) return;
      scheduleHide();
    });
  }

  _isOpenHovered() {
    if (!this.hasHover) {
      return false;
    }

    return (
      this._triggerElement.matches(":hover") || this._modal.matches(":hover")
    );
  }

  handleCustomState(initialState) {
    if (initialState === "custom") {
      const { uniqueId, delayTime, storedTime } = this._modalSettings;
      const cacheKey = uniqueId
        ? `${this._config.cachePrefix}${uniqueId}`
        : false;

      const hasStoredState = cacheKey ? this.getClosedState(cacheKey) : false;

      if (hasStoredState) {
        return;
      }

      let delayTimeValue = 0;
      if (delayTime) {
        const delayTimeInt = parseInt(delayTime, 10);

        if (!isNaN(delayTimeInt)) {
          delayTimeValue = delayTimeInt;
        }
      }

      setTimeout(() => {
        this.show();
      }, delayTimeValue * 1000);

      if (cacheKey) {
        const storedTimeRaw = storedTime;
        if (storedTimeRaw) {
          this._element.addEventListener(EVENTS.HIDDEN, () => {
            this.setClosedState(cacheKey, storedTimeRaw);
          });
        }
      }
    }
  }

  getClosedState(key) {
    let value = localStorage.getItem(key);
    if (value && value !== "permanent") {
      value = parseFloat(value);
      if (!isNaN(value)) {
        if (new Date().getTime() / 86400000 > value) {
          localStorage.removeItem(key);

          return null;
        }
      }
    }

    return value;
  }

  setClosedState(key, ttl) {
    if (ttl === "permanent") {
      localStorage.setItem(key, ttl);
    } else {
      ttl = parseFloat(ttl);
      if (!isNaN(ttl)) {
        const days = new Date().getTime() / 86400000;
        localStorage.setItem(key, days + ttl);
      }
    }
  }

  toggle(e) {
    if (e && e?.preventDefault) {
      e.preventDefault();
    }

    if (this._triggerArray?.length > 1 && isElement(e.target)) {
      let target = null;
      const { classTrigger } = this._config;
      if (classTrigger) {
        target = e.target.closest(`.${classTrigger}`);
      }

      if (!target) {
        target = e.target;
      }

      this._triggerElement = target;
    }

    //  Ignore toggle if popover is open and was triggered by hover
    if (this._isOpenHovered()) {
      return;
    }

    if (!this._isOpen) {
      this.show();
    } else {
      this.hide();
    }
  }

  show(force = false) {
    // Ignore if it is still transitioning
    if (!force && (this._isOpen || this._isTransitioning)) return;

    triggerEvent(this._element, EVENTS.SHOW);

    const { rootElement, classRootOpen } = this._config;

    this._isOpen = true;
    this._setTransitioning(true);
    this._addAriaAndToggledClass();

    if (!this._modalSettings?.disableBackdrop) {
      this._scrollBar.hide();
      this._adjustDialog();
    }

    // Add class to root element
    rootElement.classList.add(classRootOpen);

    this._setEscapeEvent();

    this._backdrop.show(this._showModal.bind(this));
  }

  _showModal() {
    const modal = this._modal;
    const triggerElement = this._triggerElement;
    const isModal = this.isModal;

    if (isModal) {
      modal.style.display = "flex";
    } else {
      modal.style.visibility = "visible";
    }
    modal.removeAttribute("aria-hidden");
    modal.setAttribute("role", "dialog");
    modal.setAttribute("aria-modal", true);

    if (this.isPopover && triggerElement) {
      const placement = this._modalSettings?.popoverPlacement ?? "bottom";
      autoUpdate(triggerElement, modal, () => {
        computePosition(triggerElement, modal, {
          strategy: "fixed",
          placement,
          middleware: [flip(), shift()],
        }).then(({ x, y }) => {
          Object.assign(modal.style, {
            left: `${x}px`,
            top: `${y}px`,
          });
        });
      });
    }

    reflow(modal);

    modal.classList.add(this._config.classShow);

    // Execute callback after transition
    this._queueCallback(
      this._transitionEnd.bind(this),
      isModal ? this._dialog : modal,
    );
  }

  hide() {
    const modal = this._modal;
    const { classShow, classAnimating } = this._config;

    // Ignore if it is still transitioning
    if (
      !this._isOpen ||
      this._isTransitioning ||
      modal.classList.contains(classAnimating) // Custom animation is running
    )
      return;

    triggerEvent(this._element, EVENTS.HIDE);

    this._isOpen = false;
    this._setTransitioning(true);
    this._addAriaAndToggledClass();

    // Remove toggle, show classes
    modal.classList.remove(classShow);

    this._setEscapeEvent();

    // Execute callback after transition
    this._queueCallback(
      this._hideModal.bind(this),
      this.isModal ? this._dialog : modal,
    );
  }

  _hideModal() {
    // If we reopened while the hide transition was running, abort this hide.
    if (this._isOpen) return;

    const modal = this._modal;

    if (this.isModal) {
      modal.style.display = "none";
    } else {
      modal.style.visibility = "hidden";
    }
    modal.setAttribute("aria-hidden", true);
    modal.removeAttribute("aria-modal");
    modal.removeAttribute("role");

    const { classAnimation, classAnimatedWaiting } = this._config;

    if (classAnimation === "custom-animation") {
      this._element.classList.add(classAnimatedWaiting);
    }

    this._backdrop.hide(this._transitionEnd.bind(this));
  }

  _getModalMarkup() {
    if (!this?._modal) {
      const modalBlock = this._element;
      const isModal = this.isModal;
      const {
        classModal,
        classAnimation,
        classModalInitialized,
        modalType,
        instanceIndex,
        classModalDialog,
        classModalContent,
        classModalHeader,
        classModalTitle,
        classDismiss,
      } = this._config;

      const {
        title,
        // hasCustomPosition,
        selector,
        modalPosition,
        cssText,
        cssTextModal,
        closeButtonColor,
        hideCloseButton,
        forceCloseButtons,
      } = this._modalSettings;

      const modal = document.createElement("div");
      modal.classList.add(
        classModal,
        classAnimation,
        classModalInitialized,
        `is-${modalType}`,
      );
      modal.setAttribute("tabindex", -1);
      if (title) {
        modal.setAttribute(
          "aria-labelledby",
          `#bb-dialog-title-${instanceIndex}`,
        );
      }

      // if (hasCustomPosition) {
      //   modal.classList.add("modal--custom-position");
      // }

      if (selector) {
        modal.classList.add(selector);
      }

      if (isModal && modalPosition) {
        modal.classList.add("modal--custom-position");
      }

      const dialog = document.createElement("div");
      dialog.classList.add(...classModalDialog.split(" "));

      if (cssText) {
        if (isModal) {
          dialog.style.cssText = cssText;
        } else {
          modal.style.cssText = cssText;
        }
      }

      if (cssTextModal) {
        if (isModal) {
          modal.style.cssText = cssTextModal;
        }
      }

      // Create modal content wrapper
      const modalContent = document.createElement("div");
      modalContent.classList.add(classModalContent);

      // Modal header
      const modalHeader = document.createElement("div");
      modalHeader.classList.add(classModalHeader);

      // Add modal title
      if (title) {
        const titleElement = document.createElement("h4");
        titleElement.classList.add(classModalTitle);
        titleElement.textContent = title;
        titleElement.setAttribute("id", `#bb-dialog-title-${instanceIndex}`);
        titleElement.style.cssText = `position: absolute;width: 1px;height: 1px;padding: 0;margin: -1px;overflow: hidden;clip: rect(0, 0, 0, 0);white-space: nowrap;border-width: 0;`;

        modalHeader.appendChild(titleElement);
        modalHeader.classList.add("has-title");
      } else {
        modalHeader.classList.add("has-no-title");
      }

      if (modalBlock.style.cssText) {
        let marginStyles = modalBlock.style.cssText.split(";");
        if (marginStyles.length > 0) {
          marginStyles = marginStyles.filter(
            (attributeName) =>
              attributeName.trim().indexOf("--bb--margin-top") !== -1 ||
              attributeName.trim().indexOf("--bb--margin-right") !== -1,
          );

          if (marginStyles.length > 0) {
            modalHeader.style.cssText = marginStyles.join(";");
          }
        }
      }

      // Add close button
      if (!hideCloseButton) {
        const closeButton = document.createElement("button");
        closeButton.innerHTML =
          '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="btn-close__icon"><path d="m13 11.8 6.1-6.3-1-1-6.1 6.2-6.1-6.2-1 1 6.1 6.3-6.5 6.7 1 1 6.5-6.6 6.5 6.6 1-1z"/></svg>';
        closeButton.setAttribute("type", "button");
        closeButton.setAttribute("aria-label", "Close");
        closeButton.classList.add("btn-close", classDismiss);

        if (closeButtonColor) {
          closeButton.style.setProperty("color", closeButtonColor);
        }

        // Close modal when clicking on close button
        closeButton.addEventListener("click", (e) => {
          e.preventDefault();
          e.stopPropagation();

          this.hide();
        });

        modalHeader.appendChild(closeButton);
      }

      // Add header to the modal content
      modalContent.appendChild(modalHeader);

      modalBlock.parentNode
        .insertBefore(modal, modalBlock)
        .appendChild(dialog)
        .appendChild(modalContent)
        .appendChild(modalBlock);

      modalBlock.classList.remove("is-modal-body");

      // Close modal when clicking outside of the modal content
      modal.addEventListener("click", (e) => {
        if (!modalContent.contains(e.target)) {
          if (forceCloseButtons) {
            this._highlightModal();
          } else {
            this.hide();
          }
        }
      });

      // Close modal when clicking on a 'close' elements inside the body
      [].slice
        .call(modalBlock.querySelectorAll(`.${classDismiss}`))
        .forEach((elem) => {
          elem.addEventListener("click", (e) => {
            e.preventDefault();

            this.hide();
          });
        });

      this._dialog = dialog;
      this._modal = modal;
    }

    return this._modal;
  }

  _setEscapeEvent() {
    const modal = this._modal;
    const onEscape = this._onEscape;
    if (this._isOpen) {
      modal.addEventListener("keydown", onEscape);
    } else {
      modal.removeEventListener("keydown", onEscape);
    }
  }

  _closeOnEscapeKey(e) {
    if (e.key === "Escape") {
      e.preventDefault();

      if (this._modalSettings?.forceCloseButtons) {
        this._highlightModal();
      } else {
        this.hide();
      }
    }
  }

  _addAriaAndToggledClass() {
    const isOpen = this._isOpen;
    const triggerArray = this._triggerArray;

    if (!!triggerArray.length) {
      const { classToggledOn, classToggledOff } = this._config;
      for (const element of triggerArray) {
        element.classList.toggle(classToggledOn, isOpen);
        element.classList.toggle(classToggledOff, !isOpen);
        element.setAttribute("aria-expanded", isOpen);
      }
    }

    if (this.hasHover) {
      this._triggerElement.classList.toggle("is-hovered", isOpen);
    }
  }

  _setTransitioning(isTransitioning) {
    this._isTransitioning = isTransitioning;
  }

  _transitionEnd() {
    // Mark the transition state as stopped
    this._setTransitioning(false);

    const modalBlock = this._element;
    const modal = this._modal;
    const modalSettings = this._modalSettings;
    const config = this._config;

    if (!this._isOpen) {
      config.rootElement.classList.remove(config.classRootOpen);

      if (!modalSettings?.disableBackdrop) {
        this._resetAdjustments();
        this._scrollBar.reset();
      }

      triggerEvent(modalBlock, EVENTS.HIDDEN);
      triggerEvent(window, EVENTS.HIDDEN, {
        target: modalBlock,
        modal,
        config,
        modalSettings,
      });

      // Focus back to the trigger if it exists
      this._focusElement(this._triggerElement);
    } else {
      modal.focus();

      // Focus the first focusable element on the target
      if (this._dataset?.toggleFocusFirstElement === "true") {
        this._focusFirstFocusableElement(modalBlock);
      }

      triggerEvent(modalBlock, EVENTS.SHOWN);
      triggerEvent(window, EVENTS.SHOWN, {
        target: modalBlock,
        modal,
        config,
        modalSettings,
      });
    }
  }

  _focusElement(elem) {
    if (!elem) {
      return;
    }

    if (elem?.firstElementChild?.tagName?.toLowerCase() === "a") {
      elem.firstElementChild.focus();
    } else if (typeof elem.focus === "function") {
      elem.focus();
    }
  }

  _focusFirstFocusableElement(elem) {
    let firstFocusableElement = elem.querySelector(this._config.selectorFocus);
    if (!firstFocusableElement) {
      firstFocusableElement = elem.querySelector(
        'button, input:not([type="hidden"]), select, textarea, a[href]',
      );
    } else {
      if (
        firstFocusableElement?.tagName === "DIV" &&
        firstFocusableElement.classList.contains("wp-block-button")
      ) {
        firstFocusableElement = firstFocusableElement.querySelector("a");
      }
    }

    if (firstFocusableElement) {
      requestAnimationFrame(() => {
        firstFocusableElement.focus();
      });
    }
  }

  _adjustDialog() {
    const modalBlock = this._element;
    const isModalOverflowing =
      modalBlock.scrollHeight > document.documentElement.clientHeight;
    const scrollbarWidth = this._scrollBar.getWidth();
    const isBodyOverflowing = scrollbarWidth > 0;

    if (isBodyOverflowing && !isModalOverflowing) {
      const property = isRTL() ? "paddingLeft" : "paddingRight";
      modalBlock.style[property] = `${scrollbarWidth}px`;
    }

    if (!isBodyOverflowing && isModalOverflowing) {
      const property = isRTL() ? "paddingRight" : "paddingLeft";
      modalBlock.style[property] = `${scrollbarWidth}px`;
    }
  }

  _resetAdjustments() {
    const modalBlock = this._element;
    modalBlock.style.paddingLeft = "";
    modalBlock.style.paddingRight = "";
  }

  _initializeBackDrop() {
    const { disableBackdrop, forceCloseButtons } = this._modalSettings;
    return new Backdrop({
      isVisible: !disableBackdrop,
      clickCallback: forceCloseButtons
        ? this._highlightModal.bind(this)
        : this.hide.bind(this),
    });
  }

  _initializeScrollbar() {
    if (!this._modalSettings?.disableBackdrop) {
      return new ScrollBarHelper();
    }

    return null;
  }

  _highlightModal() {
    const modal = this._modal;
    const initialOverflowY = modal.style.overflowY;
    const config = this._config;
    const isModal = this.isModal;
    const dialog = this._dialog;

    if (modal.classList.contains(config.classStatic)) {
      return;
    }

    modal.classList.add(config.classStatic);
    modal.style.overflowY = "hidden";

    this._queueCallback(
      () => {
        modal.classList.remove(config.classStatic);

        this._queueCallback(
          () => {
            modal.style.overflowY = initialOverflowY;
          },
          isModal ? dialog : modal,
        );
      },
      isModal ? dialog : modal,
    );
  }
}

exposeComponent("Modal", Modal);

export default Modal;
