import { Controller } from "@hotwired/stimulus";

/**
 * Stimulus controller to toggle element visibility
 * @extends Controller
 */
export default class RevealController extends Controller {
  static values = {
    open: Boolean,
    transitioning: Boolean,
    targetSelector: String,
    toggleKeys: String,
    showKeys: String,
    hideKeys: String,
    away: Boolean,
    debug: Boolean,
  };

  connect() {
    this._initCloseKeypressListener();
    this._initToggleKeypressListener();
    this._initShowKeypressListener();
    this._awayHandler = this._awayHandler.bind(this);
  }

  disconnect() {
    this._teardown();
  }

  /**
   * Shows elements connected to the controller.
   * @param {Event} event - an event with a currentTarget DOMElement
   */
  show(event) {
    if (this.openValue || this.transitioningValue) return;

    this._init(event, true);
  }

  /**
   * Hides elements connected to the controller.
   * @param {Event} event - an event with a currentTarget DOMElement
   */
  hide(event) {
    if (!this.openValue || this.transitioningValue) return;

    this._init(event, false);
  }

  /**
   * Toggles elements connected to the controller.
   * @param {Event} event - an event with a currentTarget DOMElement
   */
  toggle(event) {
    if (this.transitioningValue) return;

    this._init(event, !this.openValue);
  }

  /**
   * Stops event propagation of elements connected to the controller.
   * @param {Event} event - an event with a currentTarget DOMElement
   */
  stop(event) {
    event.stopPropagation();
  }

  // Private methods

  /**
   * @private
   * @param {Event} event
   * @param {Event} shouldOpen
   */
  async _init(event, shouldOpen) {
    if (event && event.currentTarget && event.currentTarget.dataset) {
      if ("revealPreventDefault" in event.currentTarget.dataset) {
        event.preventDefault();
      }
      if ("revealStopPropagation" in event.currentTarget.dataset) {
        event.stopPropagation();
      }
    }
    // start stuff
    const startSelector = `${this.selector}[data-${shouldOpen ? "enter" : "leave"
      }-start]`;
    const startPromises = this._didInitWithPromise(startSelector, shouldOpen);
    await Promise.all(startPromises);

    const defaultSelector = `${this.selector}:not([data-${shouldOpen ? "enter" : "leave"
      }-start]):not([data-${shouldOpen ? "enter" : "leave"}-end])`;
    const defaultPromises = this._didInitWithPromise(
      defaultSelector,
      shouldOpen
    );
    await Promise.all(defaultPromises);

    // end stuff
    const endSelector = `${this.selector}[data-${shouldOpen ? "enter" : "leave"
      }-end]`;
    const endPromises = this._didInitWithPromise(endSelector, shouldOpen);
    await Promise.all(endPromises);
  }

  /**
   * @private
   */
  _teardown() {
    if (this.hasAwayValue) {
      document.removeEventListener("click", this._awayHandler);
    }
  }

  _didInitWithPromise(selector, shouldOpen) {
    this._debug("selecting", selector, this._fetchElements(selector));
    return this._fetchElements(selector).map((element) => {
      return this._doInitTransition(element, shouldOpen);
    });
  }

  /**
   * @private
   */
  _initCloseKeypressListener() {
    if (this.hasHideKeysValue) {
      document.addEventListener("keydown", (event) => {
        if (!this.openValue) return;
        if (!this.hideKeysValue.split(",").includes(event.key.toLowerCase())) {
          return;
        }

        event.stopPropagation();
        this.toggle(event);
      });
    }
  }

  /**
   * @private
   */
  _initToggleKeypressListener() {
    if (this.hasToggleKeysValue) {
      document.addEventListener("keydown", (event) => {
        if (
          !this.toggleKeysValue.split(",").includes(event.key.toLowerCase())
        ) {
          return;
        }

        event.stopPropagation();

        this.toggle(event);
      });
    }
  }

  /**
   * @private
   */
  _initShowKeypressListener() {
    if (this.hasShowKeysValue) {
      document.addEventListener("keydown", (event) => {
        if (this.openValue) return;
        if (!this.showKeysValue.split(",").includes(event.key.toLowerCase())) {
          return;
        }

        event.stopPropagation();

        this.toggle(event);
      });
    }
  }

  /**
   * @private
   */
  _awayHandler(event) {
    if (!this.element.contains(event.target)) {
      document.removeEventListener("click", this._awayHandler);
      this.hide(event);
    }
    return true;
  }

  /**
   * @private
   * @param {DOMElement} target
   * @param {boolean} openState
   */
  _doInitTransition(target, openState) {
    this._debug("init transition", `${openState ? "open" : "closed"}`, target);
    this._debug(
      "dispatching event",
      `reveal:${openState ? "show" : "hide"}`,
      target
    );
    target.dispatchEvent(
      new Event(`reveal:${openState ? "show" : "hide"}`, {
        bubbles: true,
        cancelable: false,
      })
    );

    return new Promise((resolve, reject) => {
      if (
        "transition" in target.dataset &&
        this.element.offsetParent !== null
      ) {
        requestAnimationFrame(() => {
          this._transitionSetup(target, openState);
          const _didEndTransition = this._didEndTransition.bind(this);

          target.addEventListener(
            "transitionend",
            function _didEndTransitionHandler() {
              _didEndTransition(target, openState);
              target.removeEventListener(
                "transitionend",
                _didEndTransitionHandler
              );
              resolve();
            }
          );

          requestAnimationFrame(() => {
            this._doStartTransition(target, openState);
          });
        });
      } else {
        if (openState) {
          this._debug(
            "force hidden - init",
            `${openState ? "open" : "closed"}`,
            target
          );
          target.hidden = !target.hidden;
        }
        this._doCompleteTransition(target, openState);
        resolve();
      }
    });
  }

  /**
   * @private
   * @param {DOMElement} target
   */
  _doStartTransition(target, openState) {
    this._debug("start transition", `${openState ? "open" : "closed"}`, target);
    this.transitioningValue = true;
    if (target.dataset.useTransitionClasses === "true") {
      const transitionClasses = this._transitionClasses(
        target,
        this.transitionType
      );
      target.classList.add(...transitionClasses.end.split(" "));
      target.classList.remove(...transitionClasses.start.split(" "));
    } else {
      const transitions = this._transitionDefaults(openState);
      target.style.transformOrigin = transitions.origin;
      target.style.transitionProperty = "opacity transform";
      target.style.transitionDuration = `${transitions.duration / 1000}s`;
      target.style.transitionTimingFunction = "cubic-bezier(0.4, 0.0, 0.2, 1)";

      target.style.opacity = transitions.to.opacity;
      target.style.transform = `scale(${transitions.to.scale / 100})`;
    }
  }

  /**
   * @private
   * @param {DOMElement} target
   * @param {boolean} openState
   */
  _didEndTransition(target, openState) {
    this._debug("end transition", `${openState ? "open" : "closed"}`, target);
    if (target.dataset.useTransitionClasses === "true") {
      const transitionClasses = this._transitionClasses(
        target,
        this.transitionType
      );
      target.classList.remove(...transitionClasses.before.split(" "));
    } else {
      target.style.opacity = target.dataset.opacityCache;
      target.style.transform = target.dataset.transformCache;
      target.style.transformOrigin = target.dataset.transformOriginCache;
    }
    this._doCompleteTransition(target, openState);
  }

  /**
   * @private
   * @param {DOMElement} target
   * @param {boolean} openState
   */
  _doCompleteTransition(target, openState) {
    this._debug(
      "complete transition",
      `${openState ? "open" : "closed"}`,
      target
    );
    this.transitioningValue = false;

    if (!openState) {
      this._debug(
        "force hidden - complete",
        `${openState ? "open" : "closed"}`,
        target
      );
      target.hidden = !target.hidden;
    }
    this.openValue = openState;

    this._debug(
      "dispatching event",
      `reveal:${openState ? "shown" : "hidden"}`,
      target
    );
    target.dispatchEvent(
      new Event(`reveal:${openState ? "shown" : "hidden"}`, {
        bubbles: true,
        cancelable: false,
      })
    );

    if (this.hasAwayValue && openState) {
      document.addEventListener("click", this._awayHandler);
    }

    this._debug("dispatching event", "reveal:complete", target);
    target.dispatchEvent(
      new Event("reveal:complete", { bubbles: true, cancelable: false })
    );
  }

  /**
   * @private
   * @param {DOMElement} target
   * @param {boolean} openState
   */
  _transitionSetup(target, openState) {
    this.transitionType = openState ? "transitionEnter" : "transitionLeave";

    if (this.transitionType in target.dataset) {
      target.dataset.useTransitionClasses = true;
      const transitionClasses = this._transitionClasses(
        target,
        this.transitionType
      );
      target.classList.add(...transitionClasses.before.split(" "));
      target.classList.add(...transitionClasses.start.split(" "));
    } else {
      target.dataset.useTransitionClasses = false;
      const transitions = this._transitionDefaults(openState);
      target.dataset.opacityCache = target.style.opacity;
      target.dataset.transformCache = target.style.transform;
      target.dataset.transformOriginCache = target.style.transformOrigin;

      target.style.opacity = transitions.from.opacity;
      target.style.transform = `scale(${transitions.from.scale / 100})`;
    }
    if (openState) {
      this._debug("opening with transition", target);
      target.hidden = !target.hidden;
    }
  }

  /**
   * @private
   * @param {boolean} openState
   */
  _transitionDefaults(openState) {
    return {
      duration: openState ? 200 : 150,
      origin: "center",
      from: {
        opacity: openState ? 0 : 1,
        scale: openState ? 95 : 100,
      },
      to: {
        opacity: openState ? 1 : 0,
        scale: openState ? 100 : 95,
      },
    };
  }

  /**
   * @private
   * @param {DOMElement} target
   * @param {string} transitionType
   */
  _transitionClasses(target, transitionType) {
    return {
      before: target.dataset[transitionType],
      start: target.dataset[`${transitionType}Start`],
      end: target.dataset[`${transitionType}End`],
    };
  }

  /**
   * @private
   * @param {String} selector
   */
  _fetchElements(selector) {
    return [this.element, ...this.element.querySelectorAll(selector)].filter(
      (el) => el.matches(selector)
    );
  }

  /**
   * @private
   * @param {Array} args
   */
  _debug(...args) {
    if (this.debugValue) console.log(...args);
  }

  get selector() {
    return this.hasTargetSelectorValue
      ? this.targetSelectorValue
      : "[data-reveal]";
  }
}
