import React, {
  AriaAttributes,
  ReactElement,
  ReactNode,
  isValidElement,
  useEffect,
  useRef,
  useState,
} from 'react';

import { useTestIdAttribute } from '../../hooks/useTestIdAttribute';
import { TestIdProps } from '../../types';
import { assertEmptyObject } from '../../utils/assertEmptyObject';
import { makeTestId } from '../../utils/makeTestId';

import { TruncatedGroupContextProvider } from './contexts/TruncatedGroupContext';
import { StyledList, StyledWrapper } from './styled';

interface TruncatedGroupProps extends TestIdProps {
  /** List items */
  children: ReactNode;
  /** Element that will be rendered as 'more' button */
  more: ReactNode;
  gap: number;
  className?: string;
  ariaDescribedBy?: AriaAttributes['aria-describedby'];
}

const maxButtonOrder = 999;

/**
 * Auxiliary component for making auto-collapsible lists, with button 'more' e.g.
 */
export function TruncatedGroup(props: TruncatedGroupProps) {
  const { className, children, ariaDescribedBy, more, testId, gap, ...rest } = props;
  assertEmptyObject(rest);

  const testIdAttribute = useTestIdAttribute();

  const wrapperRef = useRef<HTMLUListElement>(null);
  const moreButtonRef = useRef<HTMLLIElement>(null);
  const firstItemRef = useRef<HTMLLIElement>(null);

  const [moreVisibility, setMoreVisibility] = useState(true);
  const [moreButtonOrder, setMoreButtonOrder] = useState(maxButtonOrder);
  const [height, setHeight] = useState(0);

  useEffect(() => {
    const buttonWrapper = moreButtonRef.current!;
    const wrapper = wrapperRef.current!;

    const childElements = () => Array.from(wrapper.children!) as HTMLElement[];

    const handleChange = () => {
      const elements = childElements();
      const items = elements.filter((item) => item !== buttonWrapper);

      let maxHeight = 0;
      elements.forEach((el) => {
        const elementHeight = Math.ceil(el.offsetHeight);
        if (elementHeight > maxHeight) {
          maxHeight = elementHeight;
        }
      });
      setHeight(maxHeight);

      const wrapperComputedStyle = getComputedStyle(wrapper);
      const wrapperWidth = parseInt(wrapperComputedStyle.width);
      const buttonWrapperWidth = buttonWrapper.offsetWidth;
      const columnGap = parseInt(wrapperComputedStyle.columnGap) || 0;

      let indexWithButton = null;
      let indexWithoutButton = null;
      let width = 0;

      for (let index = 0; index < items.length; index++) {
        width += items[index].offsetWidth;
        const widthWithGaps = width + columnGap * index;
        if (widthWithGaps + columnGap + buttonWrapperWidth >= wrapperWidth && indexWithButton === null) {
          indexWithButton = index;
        }
        if (widthWithGaps > wrapperWidth && indexWithoutButton === null) {
          indexWithoutButton = index;
        }
      }

      if (indexWithoutButton === null) {
        setMoreVisibility(false);
        setMoreButtonOrder(maxButtonOrder);
        return;
      }

      /* istanbul ignore else */
      if (indexWithButton !== null) {
        setMoreVisibility(true);
        setMoreButtonOrder(indexWithButton * 2);
      }
    };

    const resizeObserver = new ResizeObserver(handleChange);
    resizeObserver.observe(wrapper);

    const watch = (el: Element) => resizeObserver.observe(el);

    childElements().forEach(watch);

    const mutationObserver = new MutationObserver((mutationList) => {
      let removed = 0;
      mutationList.forEach((mutationRecord) => {
        removed += mutationRecord.removedNodes.length;
        mutationRecord.addedNodes.forEach((node) => {
          /* istanbul ignore else  */
          if (node.nodeType === Node.ELEMENT_NODE) {
            watch(node as Element);
          }
        });
      });
      if (removed > 0) {
        handleChange();
      }
    });
    mutationObserver.observe(wrapper, { childList: true });

    return () => {
      resizeObserver.disconnect();
      mutationObserver.disconnect();
    };
  }, []);

  const hiddenElements: ReactElement[] = [];

  let numberChilds = -1;
  const items = React.Children.map(children, (child, index) => {
    if (!child) {
      return null;
    }
    numberChilds++;
    const order = numberChilds * 2 + 1;
    const isHidden = order > moreButtonOrder;
    const visibility = isHidden ? 'hidden' : 'visible';

    if (isHidden && isValidElement(child)) {
      hiddenElements.push(child);
    }

    return (
      // Using inline style for performance improvement
      <li ref={index === 0 ? firstItemRef : undefined} aria-hidden={isHidden} style={{ order, visibility }}>
        {child}
      </li>
    );
  });

  const firstItemRefWidth = firstItemRef.current ? firstItemRef.current.offsetWidth : 0;
  const moreButtonRefWidth = moreButtonRef.current ? moreButtonRef.current.offsetWidth : 0;

  return (
    <TruncatedGroupContextProvider hiddenElements={hiddenElements}>
      <StyledWrapper
        $minWidth={firstItemRefWidth + moreButtonRefWidth + gap + 1}
        aria-describedby={ariaDescribedBy}
        className={className}
        {...{ [testIdAttribute]: testId }}
      >
        <StyledList ref={wrapperRef} style={{ height, gap }}>
          {items}
          <li
            ref={moreButtonRef}
            aria-hidden={!moreVisibility}
            {...{ [testIdAttribute]: makeTestId(testId, 'more-button-wrapper') }}
            style={{ order: moreButtonOrder, visibility: moreVisibility ? 'visible' : 'hidden' }}
          >
            {more}
          </li>
        </StyledList>
      </StyledWrapper>
    </TruncatedGroupContextProvider>
  );
}
