import { type MouseEvent, useState, useCallback, useEffect } from 'react';
import { usePagerReplacedEvent } from '@assets/filters/common/events/events';
import { validatePage } from '@assets/filters/common/utils/validatePage';
import { type ChangeFunc, getComponentSelector } from '@assets/filters/common/utils/serverBindings';
import { wpjFunction } from '@assets/components/util.event';
import { Logger } from '@assets/filters/common/utils/logger';

const logger = new Logger('usePager');

const PagerSelector = {
   main: getComponentSelector('Pager'),
   top: getComponentSelector('PagerTop'),
} as const;

// Selector for the anchor tags inside a pager component.
// Click event listeners are bound to these elements.
const SCROLL_TO_SELECTOR = [
   getComponentSelector('Section:View'),
   getComponentSelector('Section:Products'),
   getComponentSelector('Preorders:Products'),
];

const PAGER_LINK_SELECTOR = '[data-filter-page]';
const SCROLL_TOP_SELECTOR = '[data-scroll-top]';

/**
 * Binds all the (supported) pager elements on the current page.
 *    - Notifies a page change (user clicked on a link inside a pager),
 *    - returns the current page (initializes to a ?page get parameter).
 *
 * @param onChange callback which is called when a user request a page change
 * @param prefetchFunc
 */
export function usePager(onChange?: ChangeFunc<number>, prefetchFunc?: (page: number) => void) {
   const [page, setPage] = useState(1);

   const handlePageChange = useCallback((newPage: number) => {
      setPage((previousPage) => {
         // Only notify the parent component about the page change
         // when the page *actually* changes.
         if (onChange && previousPage !== newPage) {
            onChange(newPage);
         }

         return newPage;
      });
   }, [onChange]);

   useBindPagerElement(PagerSelector.main, handlePageChange, prefetchFunc);
   useBindPagerElement(PagerSelector.top, handlePageChange, prefetchFunc, false);

   // Get the current page from a GET parameter.
   // Both pagers should match, since they are server rendered.
   useEffect(() => {
      const initialPage = +(new URLSearchParams(window.location.search).get('page') || 1);

      setPage(initialPage);
   }, []);

   // Handle history back / forward navigation
   useEffect(() => {
      function popStateHandler(ev: PopStateEvent) {
         if (!('page' in ev.state)) {
            return;
         }

         const page = +ev.state.page;
         handlePageChange(page);
      }

      window.addEventListener('popstate', popStateHandler);

      return () => void window.removeEventListener('popstate', popStateHandler);
   }, []);

   return page;
}

/**
 * Bind event listeners to the links inside a pager element.
 *
 * @param selector the CSS selector of the pager elements, which wraps the links
 * @param onPageChange callback which is called when a user clicks one of the links
 * @param onHoverPrefetch
 * @param required weather to log an error, if the element is not found
 */
function useBindPagerElement(
   selector: string | null,
   onPageChange: (page: number) => void,
   onHoverPrefetch?: (page: number) => void,
   required: boolean = true,
) {
   const [pager, setPager] = useState({
      element: null as HTMLElement | null,
   });

   useScrollTopButton(pager);

   const bindPager = useCallback((ev?: Event) => {
      if (!selector) {
         return;
      }

      let newPager: HTMLElement | null;
      if (ev) {
         newPager = (ev.target as HTMLElement).closest(selector);
      } else {
         newPager = document.querySelector(selector);
      }

      if (!(newPager instanceof HTMLElement)) {
         required && logger.error(`Could not bind to pager with selector: "${selector}".`);

         return;
      }

      setPager({ element: newPager });
   }, [selector, required]);

   // bind the pager on the hook mount
   useEffect(() => {
      bindPager();
   }, []);

   // When a pager gets replaced, validate if it's the one we're bound to
   // and if so, bind to the new element.
   usePagerReplacedEvent(useCallback(({ target, updatePage }) => {
      if (selector === null || !target.matches(selector)) {
         return;
      }

      bindPager();

      if (!updatePage) {
         return;
      }

      const currentLink = target?.querySelector('.pagination .active a[data-filter-page]');
      if (!(currentLink instanceof HTMLElement)) {
         logger.log('Could not find an active link inside a replaced pager and updatePage is set to true.');

         return;
      }

      const page = validatePage(currentLink.dataset.filterPage);
      if (!page) {
         logger.error(`Invalid page number on active pager link: ${currentLink.dataset.filterPage}.`);

         return;
      }

      onPageChange(page);
   }, [selector, onPageChange]));

   // Do the page update notification:
   //    - prevent browser navigation,
   //    - validate that the page number is valid,
   //    - only then notify the parent component.
   const pageNavigateListener = useCallback((ev: MouseEvent<HTMLElement>) => {
      // prevent browser navigation
      ev.preventDefault();

      // try to scroll to the top of the product list
      let i = 0;
      while (i < SCROLL_TO_SELECTOR.length && !scrollTo(SCROLL_TO_SELECTOR[i++])) {
      }

      const maybePage = validatePage(ev.currentTarget.dataset.filterPage);
      if (!maybePage) {
         logger.error(`Invalid target page number on pager link element: "${maybePage}".`);

         return;
      }

      onPageChange.call(ev.currentTarget, maybePage);
   }, [onPageChange]);

   // actually bind the pager links click listeners
   useEffect(() => {
      if (!pager.element) {
         return;
      }

      const abortController = new AbortController();

      function hoverHandler(ev: Event) {
         if (!onHoverPrefetch) {
            return;
         }

         onHoverPrefetch(+(ev.currentTarget as HTMLElement).dataset.filterPage);
      }

      // register event listeners on all links inside the pager component
      const links = pager.element.querySelectorAll(PAGER_LINK_SELECTOR) as NodeListOf<HTMLElement>;
      for (const link of links) {
         link.addEventListener('click', pageNavigateListener as unknown as EventListener, { signal: abortController.signal });

         if (onHoverPrefetch
            && typeof link.dataset.prefetch !== 'undefined'
            && !link.classList.contains('disabled')
         ) {
            link.addEventListener('mouseover', hoverHandler, { signal: abortController.signal });
         }
      }

      return () => void abortController.abort();
   }, [pager, pageNavigateListener, onHoverPrefetch]);
}

function useScrollTopButton(pager: { element: HTMLElement | null }) {
   useEffect(() => {
      if (!pager.element) {
         return;
      }

      const scrollTopButton = pager.element.querySelector(SCROLL_TOP_SELECTOR);
      if (!(scrollTopButton instanceof HTMLElement)) {
         return;
      }

      function doScrollTop() {
         let i = 0;
         while (i < SCROLL_TO_SELECTOR.length && !scrollTo(SCROLL_TO_SELECTOR[i++])) {
         }
      }

      scrollTopButton.addEventListener('click', doScrollTop);

      return () => scrollTopButton.removeEventListener('click', doScrollTop);
   }, [pager]);
}

function scrollTo(referenceElementSelector: string | null) {
   if (!referenceElementSelector) {
      return;
   }

   const target = document.querySelector(referenceElementSelector);
   if (!target) {
      return false;
   }

   wpjFunction.scrollTo(target, -100);

   return true;
}
