import { getDeepestActiveElement } from './active-elements.js';
import { getTabbableElements } from './tabbable.js';

let activeModals: HTMLElement[] = [];

export default class Modal {
  element: HTMLElement;
  isExternalActivated: boolean;
  tabDirection: 'forward' | 'backward' = 'forward';
  currentFocus: HTMLElement | null;
  previousFocus: HTMLElement | null;
  elementsWithTabbableControls: string[];

  constructor(element: HTMLElement) {
    this.element = element;

    this.elementsWithTabbableControls = ['iframe'];
  }

  /** Activates focus trapping. */
  activate() {
    activeModals.push(this.element);
    document.addEventListener('focusin', this.handleFocusIn);
    document.addEventListener('keydown', this.handleKeyDown);
    document.addEventListener('keyup', this.handleKeyUp);
  }

  /** Deactivates focus trapping. */
  deactivate() {
    activeModals = activeModals.filter(modal => modal !== this.element);
    this.currentFocus = null;
    document.removeEventListener('focusin', this.handleFocusIn);
    document.removeEventListener('keydown', this.handleKeyDown);
    document.removeEventListener('keyup', this.handleKeyUp);
  }

  /** Determines if this modal element is currently active or not. */
  isActive() {
    // The "active" modal is always the most recent one shown
    return activeModals[activeModals.length - 1] === this.element;
  }

  /** Activates external modal behavior and temporarily disables focus trapping. */
  activateExternal() {
    this.isExternalActivated = true;
  }

  /** Deactivates external modal behavior and re-enables focus trapping. */
  deactivateExternal() {
    this.isExternalActivated = false;
  }

  private checkFocus() {
    if (this.isActive() && !this.isExternalActivated) {
      const tabbableElements = getTabbableElements(this.element);
      if (!this.element.matches(':focus-within')) {
        const start = tabbableElements[0];
        const end = tabbableElements[tabbableElements.length - 1];
        const target = this.tabDirection === 'forward' ? start : end;

        if (typeof target?.focus === 'function') {
          this.currentFocus = target;
          target.focus({ preventScroll: false });
        }
      }
    }
  }

  private handleFocusIn = () => {
    if (!this.isActive()) return;
    this.checkFocus();
  };

  private possiblyHasTabbableChildren(element: HTMLElement) {
    return (
      this.elementsWithTabbableControls.includes(element.tagName.toLowerCase()) || element.hasAttribute('controls')
      // Should we add a data-attribute for people to set just in case they have an element where we don't know if it has possibly tabbable elements?
    );
  }

  private handleKeyDown = (event: KeyboardEvent) => {
    if (event.key !== 'Tab' || this.isExternalActivated) return;
    if (!this.isActive()) return;

    // Because sometimes focus can actually be taken over from outside sources,
    // we don't want to rely on `this.currentFocus`. Instead we check the actual `activeElement` and
    // recurse through shadowRoots.
    const currentActiveElement = getDeepestActiveElement();
    this.previousFocus = currentActiveElement as HTMLElement | null;

    if (this.previousFocus && this.possiblyHasTabbableChildren(this.previousFocus)) {
      return;
    }

    if (event.shiftKey) {
      this.tabDirection = 'backward';
    } else {
      this.tabDirection = 'forward';
    }

    const tabbableElements = getTabbableElements(this.element);

    let currentFocusIndex = tabbableElements.findIndex(el => el === currentActiveElement);

    this.previousFocus = this.currentFocus;

    if (currentFocusIndex === -1) {
      this.currentFocus = tabbableElements[0];

      // We don't call event.preventDefault() here because it messes with tabbing to the <iframe> controls.
      // We just wait until the current focus is no longer an element with possible hidden controls.
      if (Boolean(this.previousFocus) && this.possiblyHasTabbableChildren(this.previousFocus!)) {
        return;
      }

      event.preventDefault();
      this.currentFocus?.focus({ preventScroll: false });
      return;
    }

    const addition = this.tabDirection === 'forward' ? 1 : -1;

    if (currentFocusIndex + addition >= tabbableElements.length) {
      currentFocusIndex = 0;
    } else if (currentFocusIndex + addition < 0) {
      currentFocusIndex = tabbableElements.length - 1;
    } else {
      currentFocusIndex += addition;
    }

    this.previousFocus = this.currentFocus;
    const nextFocus = /** @type {HTMLElement} */ tabbableElements[currentFocusIndex];

    // This is a special case. We need to make sure we're not calling .focus() if we're already focused on an element
    // that possibly has "controls"
    if (this.tabDirection === 'backward') {
      if (this.previousFocus && this.possiblyHasTabbableChildren(this.previousFocus)) {
        return;
      }
    }

    if (nextFocus && this.possiblyHasTabbableChildren(nextFocus)) {
      return;
    }

    event.preventDefault();
    this.currentFocus = nextFocus;
    this.currentFocus?.focus({ preventScroll: true });

    setTimeout(() => this.checkFocus());
  };

  private handleKeyUp = () => {
    this.tabDirection = 'forward';
  };
}
