// 直前に hide() で記録した display 値を要素ごとに保持する
const displayStates: WeakMap<HTMLElement, string> = new WeakMap();

/**
 * 要素を非表示にする。元の display 値を記録しておくことで、show() で正しい値に復元できる。
 */
export function hide(element: HTMLElement): void {
  const { display } = getComputedStyle(element);

  if (display !== 'none') {
    displayStates.set(element, display);
    element.style.display = 'none';
  }
}

/**
 * 要素を表示状態に戻す。
 *
 * - HTML 側で `style="display: none"` が指定されているケース（初期表示のちらつき防止用途）では
 *   インラインスタイルを外して CSS 定義に委ねるため、display が flex / grid などでも崩れない。
 * - JS の hide() で非表示にしたケースでは、記録しておいた元の display 値を復元する。
 */
export function show(element: HTMLElement): void {
  if (getComputedStyle(element).display !== 'none') {
    return;
  }

  // hide() で記録した値があれば最優先で復元
  const stored = displayStates.get(element);
  if (stored !== undefined) {
    element.style.display = stored;
    return;
  }

  // 記録がない（＝HTML 側で style="display: none" を直に書いたケース）はインライン指定を外して CSS に委ねる
  if (element.style.display === 'none') {
    element.style.removeProperty('display');
  }

  // CSS でも非表示なら block を最終フォールバック
  if (getComputedStyle(element).display === 'none') {
    element.style.display = 'block';
  }
}

/**
 * Properly escape IDs selectors to handle weird IDs
 * @param {string} selector
 * @returns {string}
 */
export function parseSelector(selector: string) {
  if (selector) {
    // document.querySelector needs escaping to handle IDs (html5+) containing for instance /
    selector = selector.replace(/#([^\s"#']+)/g, (_, id) => `#${CSS.escape(id)}`);
  }

  return selector;
}

export function getSelector(element: HTMLElement): string | null {
  let selector = element.getAttribute('data-target');

  if (!selector || selector === '#') {
    let hrefAttribute = element.getAttribute('href');

    // The only valid content that could double as a selector are IDs or classes,
    // so everything starting with `#` or `.`. If a "real" URL is used as the selector,
    // `document.querySelector` will rightfully complain it is invalid.
    // See https://github.com/twbs/bootstrap/issues/32273
    if (!hrefAttribute || (!hrefAttribute.includes('#') && !hrefAttribute.startsWith('.'))) {
      return null;
    }

    // Just in case some CMS puts out a full URL with the anchor appended
    if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) {
      hrefAttribute = `#${hrefAttribute.split('#')[1]}`;
    }

    selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null;
  }

  return selector
    ? selector
        .split(',')
        .map((sel) => parseSelector(sel))
        .join(',')
    : null;
}

export function isDisabled(element: HTMLElement): boolean {
  if (!element || element.nodeType !== Node.ELEMENT_NODE) {
    return true;
  }

  if (element.classList.contains('disabled')) {
    return true;
  }

  if (element instanceof HTMLButtonElement && typeof element.disabled !== 'undefined') {
    return element.disabled;
  }

  return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false';
}

/**
 * Return the previous/next element of a list.
 *
 * @param {array} list    The list of elements
 * @param activeElement   The active element
 * @param shouldGetNext   Choose to get next or previous element
 * @param isCycleAllowed
 * @return {Element|elem} The proper element
 */
export function getNextActiveElement(
  list: HTMLElement[],
  activeElement: HTMLElement,
  shouldGetNext: boolean,
  isCycleAllowed: boolean
): HTMLElement | null {
  const listLength = list.length;
  let index = list.indexOf(activeElement);

  // if the element does not exist in the list return an element
  // depending on the direction and if cycle is allowed
  if (index === -1) {
    return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0];
  }

  index += shouldGetNext ? 1 : -1;

  if (isCycleAllowed) {
    index = (index + listLength) % listLength;
  }

  return list[Math.max(0, Math.min(index, listLength - 1))];
}
