import React, { useEffect, useLayoutEffect, useRef, useState, useMemo, useCallback, SyntheticEvent } from 'react';
import { createPortal } from 'react-dom';
import { useClickAway, useKeyPressEvent } from 'react-use';
import { bem } from 'utils/bem';
import { noop } from 'lodash';
import { Button, ButtonVariant } from '../button';
import { Icon } from '../icon';
import { SDropdown, SDropdownList, SDropdownOption } from './s-dropdown';
import { ITEM_LINE_HEIGHT, ITEM_VERTICAL_PADDING, LIST_VERTICAL_PADDING } from './constants';
import { getNearestScrollableParentElement } from 'utils/dom';
import { navigateModTo } from 'utils/number';
import { getDropdownPosition } from './transducers';
import { Optional } from 'backend-api/models';

const dropdownContainer = document.getElementById('app_dropdowns');
export interface OptionProps {
  children?: React.ReactNode;
  onClick(event: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
  onMouseEnter?(event: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
  disabled?: boolean;
  dataSelector?: string;
  className?: string;
}

const classes = bem('dropdown');

const Option = ({ children, onClick, onMouseEnter, disabled, dataSelector, className }: OptionProps) => {
  return (
    <SDropdownOption
      onClick={onClick}
      onMouseEnter={onMouseEnter}
      data-selector={dataSelector}
      className={classes('option', { disabled: disabled || false }, className)}
    >
      {children}
    </SDropdownOption>
  );
};

export enum Direction {
  TopLeft = 'top-left',
  TopRight = 'top-right',
  BottomLeft = 'bottom-left',
  BottomRight = 'bottom-right',
}

export interface DropdownPosition {
  top: number;
  left: number;
}

interface Props {
  children: Array<React.ReactElement<OptionProps>> | React.ReactElement<OptionProps>;
  title?: React.ReactChild;
  dropdownClassName?: string;
  buttonClassName?: string;
  trigger?: React.ReactNode;
  initialDirection?: Direction;
  variant?: ButtonVariant;
  disabled?: boolean;
  dataSelector?: string;
  className?: string;
  bgStyle?: string;
}

export const Dropdown = ({
  className,
  children,
  title,
  buttonClassName,
  trigger,
  dropdownClassName,
  variant,
  disabled,
  dataSelector,
  bgStyle,
  initialDirection,
}: Props) => {
  const refButton = useRef<HTMLButtonElement>(null);
  const refWrapper = useRef<HTMLDivElement>(null);
  const refDropdown = useRef<HTMLDivElement>(null);
  const [isOpened, setIsOpened] = useState(false);
  const [focused, setFocus] = useState<Optional<number>>(undefined);
  const [dropdownPosition, setDropdownPosition] = useState<DropdownPosition>({
    top: 0,
    left: 0,
  });

  const optionsLength = React.Children.count(children);
  const itemHeight = ITEM_LINE_HEIGHT + ITEM_VERTICAL_PADDING * 2;
  const menuHeight = itemHeight * optionsLength + LIST_VERTICAL_PADDING * 2;

  const currentButtonRef = useMemo(() => (trigger ? refWrapper : refButton), [trigger, refWrapper, refButton]);

  const onClick = useCallback(
    (event: SyntheticEvent) => {
      setIsOpened(!isOpened);
      event.stopPropagation();
    },
    [isOpened]
  );

  const navigate = useMemo(() => navigateModTo(optionsLength), [optionsLength]);

  const highlightNext = useCallback(() => {
    if (!isOpened) return;

    const next = navigate(typeof focused === 'number' ? focused + 1 : 0);
    const nextElement = refDropdown.current?.childNodes?.[next];
    const isDisabled = !nextElement || (nextElement as HTMLElement).classList.contains('dropdown__option--disabled');

    if (isDisabled) return;

    setFocus(next);
  }, [isOpened, navigate, focused, refDropdown]);

  const highlightPrev = useCallback(() => {
    if (!isOpened) return;

    const prev = navigate(typeof focused === 'number' ? focused - 1 : -1);
    const prevElement = refDropdown.current?.childNodes?.[prev];
    const isDisabled = !prevElement || (prevElement as HTMLElement).classList.contains('dropdown__option--disabled');

    if (isDisabled) return;

    setFocus(prev);
  }, [isOpened, navigate, focused, refDropdown]);

  const closeDropdown = useCallback(() => {
    setIsOpened(false);
    setFocus(undefined);
  }, []);

  useClickAway(currentButtonRef, e => {
    if (!isOpened) return;

    if (!e.target) {
      closeDropdown();
      return;
    }

    let target = e.target as HTMLElement;
    let option = false;

    while (!['HTML', 'BODY'].includes(target.nodeName)) {
      if (target.classList.contains('dropdown__list')) {
        // on dropdown option click
        option = true;
      }

      if (!target.nodeName) break;
      target = target.parentNode as HTMLElement;
    }

    if (!option) closeDropdown();
  });

  // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
  useKeyPressEvent('ArrowDown', highlightNext);
  useKeyPressEvent('ArrowUp', highlightPrev);
  useKeyPressEvent('Escape', closeDropdown);
  useKeyPressEvent('Enter', () => {
    if (!isOpened) return;

    if (typeof focused === 'number') {
      const target = refDropdown.current?.childNodes[focused];
      // https://javascript.info/dispatch-events
      const event = new MouseEvent('click', {
        // view: window,
        bubbles: true,
        cancelable: true,
      });

      target?.dispatchEvent(event);
    }
  });

  const onMouseListLeave = useCallback(() => {
    setFocus(undefined);
  }, []);

  useEffect(() => {
    let scrollableContainer: HTMLElement | Window = window;

    if (isOpened) {
      const nearestScrollableContainer = getNearestScrollableParentElement(currentButtonRef.current);

      if (nearestScrollableContainer) {
        scrollableContainer = nearestScrollableContainer;
      }

      scrollableContainer.addEventListener('scroll', closeDropdown);
      window.addEventListener('resize', closeDropdown);
    }

    return () => {
      scrollableContainer.removeEventListener('scroll', closeDropdown);
      window.removeEventListener('resize', closeDropdown);
    };
  }, [closeDropdown, isOpened, currentButtonRef]);

  useLayoutEffect(() => {
    const buttonElement = currentButtonRef.current;
    const dropdownElement = refDropdown.current;

    if (buttonElement && dropdownElement) {
      const position = getDropdownPosition(
        buttonElement.getBoundingClientRect(),
        dropdownElement.offsetWidth,
        menuHeight,
        window,
        initialDirection
      );

      setDropdownPosition(position);
    }
  }, [currentButtonRef, initialDirection, isOpened, menuHeight]);

  return (
    <SDropdown className={classes('dropdown', undefined, className)} bgStyle={bgStyle}>
      {trigger ? (
        <div ref={refWrapper}>
          <Button className={buttonClassName} variant="transparent" onClick={onClick} dataSelector={dataSelector}>
            {trigger}
          </Button>
        </div>
      ) : (
        <Button
          onClick={onClick}
          ref={refButton}
          variant={variant}
          disabled={disabled}
          dataSelector={dataSelector}
          className={classes('button', undefined, buttonClassName)}
        >
          {title} <Icon name={isOpened ? 'up' : 'down'} />
        </Button>
      )}
      {dropdownContainer &&
        createPortal(
          <SDropdownList className={className} position={dropdownPosition} closed={!isOpened}>
            <div className={classes('list', undefined, dropdownClassName)} onMouseLeave={onMouseListLeave}>
              <div className={classes('options')} ref={refDropdown}>
                {React.Children.map(children, (el, i) => {
                  const { onClick, disabled, className } = el.props;

                  return React.cloneElement(el, {
                    onClick: disabled
                      ? noop
                      : (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
                          event.stopPropagation();
                          onClick(event);
                          setTimeout(closeDropdown);
                        },
                    onMouseEnter: () => setFocus(disabled ? undefined : i),
                    className: classes('option', { focused: i === focused }, className),
                  });
                })}
              </div>
            </div>
          </SDropdownList>,
          dropdownContainer
        )}
    </SDropdown>
  );
};

Dropdown.Option = Option;
