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

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

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

    // Legacy compatibility
    this.element = this._element;

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

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

    const {
      toggleElement,
      transitionProperty = "height",
      toggleState,
      toggleStateShown,
    } = this._dataset;

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

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

    // Get transition properties
    this.transitionProperty = transitionProperty;
    this.calculatedTransitionProperty =
      ["width", "height"].indexOf(transitionProperty) !== -1
        ? `scroll${
            transitionProperty[0].toUpperCase() + transitionProperty.slice(1)
          }`
        : "";

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

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

    // Initialize
    this.init();
  }

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

  defaultConfig() {
    return {
      triggerArray: null,
      dataset: null,
      datasetElement: this._element,
      classTarget: "collapse",
      classTrigger: "",
      classShow: "show",
      classCollapsing: "collapsing",
      classToggledOn: "is-toggled-on",
      classToggledOff: "is-toggled-off",
      selectorTrigger: ".js-toggle-collapse",
      selectorText: ".toggle__text",
      instanceIndex: 0,
    };
  }

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

    // Add toggled classes and accessibility attribute to the trigger elements
    this._addAriaAndToggledClass();

    // Add click handler
    this._attachClickHandler();

    // Initial state
    if (this.isOpen) {
      requestAnimationFrame(() => {
        triggerEvent(window, EVENTS.SHOWN, {
          target: this._element,
        });
      });
    }

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

  _setupClasses() {
    const element = this._element;
    const triggerArray = this._triggerArray;
    const { classTrigger, classTarget, classShow, instanceIndex } =
      this._config;

    // Add transition property to target elements
    element.classList.add(`transition-${this.transitionProperty}`);

    // Add toggle class to the target elements
    if (!element.classList.contains(classTarget)) {
      element.classList.add(classTarget);
    }

    const isOpen = this.isOpen;
    element.setAttribute("aria-expanded", isOpen);
    element.classList.toggle(classShow, isOpen);

    let collapseId = element.getAttribute("id");
    if (!collapseId) {
      collapseId = `bb-collapse-${instanceIndex}`;
      element.setAttribute("id", collapseId);
    }

    // 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-controls", `#${collapseId}`);

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

  _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);
      }
    }
  }

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

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

  show() {
    // Ignore if it is still transitioning
    if (this.isOpen || this._isTransitioning) return;

    const element = this._element;
    const { classCollapsing, classTarget } = this._config;
    const transitionProperty = this.transitionProperty;

    triggerEvent(element, EVENTS.SHOW);

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

    // Remove the toggle class
    element.classList.remove(classTarget);

    // Add transitioning class
    element.classList.add(classCollapsing);

    // Set inline style zero
    element.style[transitionProperty] = 0;

    // Execute callback after transition
    this._queueCallback(this._transitionEnd.bind(this), element);

    // Set inline style value
    element.style[transitionProperty] = `${
      element[this.calculatedTransitionProperty]
    }px`;
  }

  hide() {
    // Ignore if it is still transitioning
    if (!this.isOpen || this._isTransitioning) return;

    const element = this._element;
    const { classCollapsing, classTarget, classShow } = this._config;
    const transitionProperty = this.transitionProperty;

    triggerEvent(element, EVENTS.HIDE);

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

    // Set inline style
    element.style[transitionProperty] = `${
      element[this.calculatedTransitionProperty]
    }px`;

    reflow(element);

    // Mark the element is about to toggle
    element.classList.add(classCollapsing);

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

    // Clear transition value
    element.style[transitionProperty] = "";

    // Execute callback after transition
    this._queueCallback(this._transitionEnd.bind(this), element);
  }

  _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);
      }
    }
  }

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

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

    const element = this._element;
    const { classCollapsing, classTarget, classShow } = this._config;
    const isOpen = this.isOpen;

    element.setAttribute("aria-expanded", isOpen);

    // Remove the transitioning class
    element.classList.remove(classCollapsing);

    if (isOpen) {
      element.style[this.transitionProperty] = "";
      element.classList.add(classTarget, classShow);

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

      triggerEvent(element, EVENTS.SHOWN);
      triggerEvent(window, EVENTS.SHOWN, {
        target: element,
      });
    } else {
      element.classList.add(classTarget);
      triggerEvent(element, EVENTS.HIDDEN);
      triggerEvent(window, EVENTS.HIDDEN, {
        target: element,
      });
    }
  }

  _focusFirstFocusableElement(elem) {
    const firstFocusableElement = elem.querySelector(
      'button, input:not([type="hidden"]), select, textarea',
    );

    if (firstFocusableElement) {
      firstFocusableElement.focus();
    }
  }

  dispose() {
    super.dispose();

    const triggerArray = this._triggerArray;
    const { classToggledOn, classToggledOff, classShow } = this._config;

    if (!!triggerArray.length) {
      for (const element of triggerArray) {
        element.classList.remove(classToggledOn);
        element.classList.remove(classToggledOff);
        element.removeEventListener("click", this.clickHandler);
      }
    }

    this._element.classList.add(classShow);
  }
}

exposeComponent("Collapse", Collapse);

export default Collapse;
