import { ApplicationController } from './application_controller';

export class MenuButtonController extends ApplicationController {
  static targets = ['button', 'menu'];

  declare readonly buttonTarget: HTMLButtonElement;
  declare readonly menuTarget: HTMLElement;

  #isOpen = false;
  #teardown?: () => void;

  connect() {
    this.setup();
  }

  disconnect(): void {
    this.#teardown?.();
  }

  private get isMenu() {
    return !(this.element as HTMLElement).dataset.popover;
  }

  private setup() {
    this.buttonTarget.setAttribute(
      'aria-haspopup',
      this.isMenu ? 'menu' : 'true'
    );
    this.buttonTarget.setAttribute('aria-controls', this.menuTarget.id);
    if (!this.buttonTarget.id) {
      this.buttonTarget.id = `${this.menuTarget.id}_button`;
    }

    this.menuTarget.setAttribute('aria-labelledby', this.buttonTarget.id);
    this.menuTarget.setAttribute('role', this.isMenu ? 'menu' : 'region');
    this.menuTarget.classList.add('fade-in-down');
    this.menuTarget.setAttribute('tab-index', '-1');

    if (this.isMenu) {
      for (const menuItem of this.menuTarget.querySelectorAll('a')) {
        menuItem.setAttribute('role', 'menuitem');
      }
    }

    this.on('click', (event) => {
      const target = event.target as HTMLElement;
      if (this.buttonTarget == target || this.buttonTarget.contains(target)) {
        event.preventDefault();

        if (this.#isOpen) {
          this.close();
        } else {
          this.open();
        }
      }
    });
    this.on('keydown', (event: KeyboardEvent) => {
      const target = event.target as HTMLElement;
      if (this.buttonTarget == target) {
        this.onButtonKeydown(event);
      } else if (
        this.isMenu &&
        (this.menuTarget == target || this.menuTarget.contains(target))
      ) {
        this.onMenuKeydown(event);
      }
    });
  }

  private open(focusMenuItem: 'first' | 'last' = 'first') {
    this.buttonTarget.setAttribute('aria-expanded', '');
    this.menuTarget.parentElement?.classList.add('open');
    this.menuTarget.focus();

    const onClickBody = (event: Event) => {
      const target = event.target as HTMLElement;
      if (this.isClickOutside(target)) {
        this.menuTarget.classList.remove('fade-in-down');
        this.close();
      }
    };
    requestAnimationFrame(() => {
      if (focusMenuItem == 'first') {
        this.setFocusToFirstMenuitem();
      } else {
        this.setFocusToLastMenuitem();
      }
      document.body.addEventListener('click', onClickBody);
    });

    this.#isOpen = true;
    this.#teardown = () =>
      document.body.removeEventListener('click', onClickBody);
  }

  private close() {
    this.buttonTarget.removeAttribute('aria-expanded');
    this.menuTarget.parentElement?.classList.remove('open');
    this.#teardown?.();
    this.setFocusToMenuitem(null);
    this.#isOpen = false;
  }

  private isClickOutside(target: HTMLElement) {
    return (
      target.isConnected &&
      !this.element.contains(target) &&
      !target.closest('reach-portal') &&
      this.#isOpen
    );
  }

  private get currentMenuItem() {
    return this.menuTarget.querySelector<HTMLElement>(
      '[role="menuitem"]:focus'
    );
  }

  private get menuItems() {
    return [
      ...this.menuTarget.querySelectorAll<HTMLElement>('[role="menuitem"]')
    ];
  }

  private setFocusToMenuitem(menuItem: HTMLElement | null) {
    if (menuItem) {
      menuItem.focus();
    } else {
      this.buttonTarget.focus();
    }
  }

  private setFocusToFirstMenuitem() {
    this.setFocusToMenuitem(this.menuItems[0]);
  }

  private setFocusToLastMenuitem() {
    const length = this.menuItems.length;
    this.setFocusToMenuitem(this.menuItems[length - 1]);
  }

  setFocusToPreviousMenuitem() {
    const { currentMenuItem, menuItems } = this;

    if (currentMenuItem) {
      const index = menuItems.indexOf(currentMenuItem);
      if (index == 0) {
        this.setFocusToLastMenuitem();
      } else {
        this.setFocusToMenuitem(menuItems[index - 1]);
      }
    }
  }

  setFocusToNextMenuitem() {
    const { currentMenuItem, menuItems } = this;

    if (currentMenuItem) {
      const index = menuItems.indexOf(currentMenuItem);
      if (index == menuItems.length - 1) {
        this.setFocusToFirstMenuitem();
      } else {
        this.setFocusToMenuitem(menuItems[index + 1]);
      }
    }
  }

  performMenuAction(target: EventTarget | null) {
    target?.dispatchEvent(new Event('click'));
  }

  private onButtonKeydown(event: KeyboardEvent) {
    let stopPropagation = false;
    switch (event.key) {
      case ' ':
      case 'Enter':
      case 'ArrowDown':
      case 'Down':
        this.open();
        stopPropagation = true;
        break;
      case 'Esc':
      case 'Escape':
        this.close();
        stopPropagation = true;
        break;
      case 'Up':
      case 'ArrowUp':
        this.open('last');
        stopPropagation = true;
        break;
      default:
        break;
    }

    if (stopPropagation) {
      event.stopPropagation();
      event.preventDefault();
    }
  }

  onMenuKeydown(event: KeyboardEvent) {
    let stopPropagation = false;
    if (event.ctrlKey || event.altKey || event.metaKey) {
      return;
    }

    if (event.shiftKey) {
      if (event.key == 'Tab') {
        this.close();
        stopPropagation = true;
      }
    } else {
      switch (event.key) {
        case ' ':
          this.performMenuAction(event.target);
          stopPropagation = true;
          break;
        case 'Esc':
        case 'Escape':
          this.close();
          stopPropagation = true;
          break;
        case 'Up':
        case 'ArrowUp':
          this.setFocusToPreviousMenuitem();
          stopPropagation = true;
          break;
        case 'ArrowDown':
        case 'Down':
          this.setFocusToNextMenuitem();
          stopPropagation = true;
          break;
        case 'Home':
        case 'PageUp':
          this.setFocusToFirstMenuitem();
          stopPropagation = true;
          break;
        case 'End':
        case 'PageDown':
          this.setFocusToLastMenuitem();
          stopPropagation = true;
          break;
        case 'Tab':
          this.close();
          break;
        default:
          break;
      }
    }

    if (stopPropagation) {
      event.stopPropagation();
      event.preventDefault();
    }
  }
}