import { useKeyUp } from '@vev/utils';
import { clamp } from 'lodash';
import React, { useCallback, useContext, useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import {
  isInside,
  isInsideRole,
  MouseStateEvent,
  useHTMLElement,
  useInterval,
  useMouseDown,
  useParentScroll,
  useSize,
  useWindowResize,
} from '../../hooks';
import { usePopoverContext } from './popover-context';
import styles from './silke-popover.scss';

export type PopoverOrigin =
  | 'top-left'
  | 'top-center'
  | 'top-right'
  | 'center-right'
  | 'bottom-right'
  | 'bottom-center'
  | 'bottom-left'
  | 'center-left'
  | 'center-center';

export type PopoverAnchor = React.RefObject<HTMLElement> | [x: number, y: number];

const PopoverWrapperContext = React.createContext<React.RefObject<HTMLElement>>({ current: null });
export const SilkePopoverContext = ({
  target,
  children,
}: {
  target: React.RefObject<HTMLElement>;
  children: React.ReactElement;
}) => {
  return <PopoverWrapperContext.Provider value={target}>{children}</PopoverWrapperContext.Provider>;
};
const OriginMap: Record<string, number> = {
  top: 0,
  bottom: 1,
  left: 0,
  right: 1,
  center: 0.5,
};

export interface SilkePopoverProps {
  /** Element or position to anchor the popover to, if not defined the center of screen is used */
  anchor?: PopoverAnchor;
  /** Children to add inside the popover */
  children?: React.ReactElement | React.ReactElement[];
  /**
   * If popover belongs to a group of popovers,
   *  using contextId will close all other with same id when hide = false on this
   * @example overflow menu - only one overflow menu should be one at any time
   * */
  contextId?: string;
  /** SHould the popover be hidden */
  hide?: boolean;
  /** What point of the target should be used for centering default center-center  */
  targetOrigin?: PopoverOrigin;
  /** What point of the anchor should be used for centering default center-center  */
  anchorOrigin?: PopoverOrigin;
  /** Offset the popover position */
  offsetX?: number;
  /** Offset the popover position */
  offsetY?: number;
  /** If transforming popover children, don't want popover container to be clickable  */
  pointerEventsNone?: boolean;
  /** Z-index of popover */
  zIndex?: number;
  /** Set the target width to the same width of anchor */
  matchAnchorWidth?: boolean;
  /** Set the target height to the same height of anchor  */
  matchAnchorHeight?: boolean;
  /** Disabled request close on click outside */
  disabledClickOutside?: boolean;
  translateAnimation?: boolean;
  shadow?: 'level1' | 'level2' | 'level3' | 'level4';
  /** Triggered when it's desired that the popover is hidden */
  onRequestClose?: (event?: MouseStateEvent) => void;
  onKeyUp?: () => void;
}

const PopoverAttrs = {
  className: styles.popover,
  role: 'popover',
};

export function SilkePopover({
  anchor,
  children,
  contextId,
  hide,
  targetOrigin,
  anchorOrigin,
  offsetX,
  offsetY,
  pointerEventsNone,
  matchAnchorWidth,
  matchAnchorHeight,
  disabledClickOutside,
  zIndex,
  shadow,
  translateAnimation,
  onRequestClose,
}: SilkePopoverProps) {
  const targetId = contextId || 'modal';
  const parentContext = useContext(PopoverWrapperContext);
  const isMountedAndOpenRef = useRef<boolean>(false);

  const attrs: Record<string, string> = { ...PopoverAttrs, id: targetId };
  if (shadow) attrs['data-shadow'] = shadow;

  const targetRef = useHTMLElement('div', attrs, !hide ? document.body : undefined);

  useEffect(() => {
    // Need timeout ensure mouse down is not triggered on the popover right away
    setTimeout(() => {
      isMountedAndOpenRef.current = !hide;
    });
  }, [hide]);

  const updatePos = () => {
    if (!hide) {
      applyPopoverStyles(
        targetRef.current as HTMLElement,
        anchor,
        anchorOrigin,
        targetOrigin,
        offsetX,
        offsetY,
        zIndex,
        matchAnchorWidth,
        matchAnchorHeight,
        pointerEventsNone,
        translateAnimation,
      );
    }
  };

  // Update position if anchor changes
  useEffect(() => {
    // Hack to fix issue when matchAnchorWidth is true
    updatePos();
    updatePos();
  }, [anchor]);

  // Register the popover context (used to prevent multiple of same popover to open at same time)
  usePopoverContext(contextId, hide, onRequestClose);
  // Update position on size change of target
  useSize(targetRef, updatePos);
  useWindowResize(updatePos);
  // Interval update pos, fallback mechanism for updating position (in case size and parent scroll fails)
  useInterval(updatePos, hide ? 0 : 1000);
  // Update position of popover if target has a parent which scrolls
  useParentScroll(!hide && isRefObject(anchor) ? anchor : null, updatePos);

  // Request close on click outside (disabled if disabled click outside or hidden or not requestClose function)
  useMouseDown(
    (e) => {
      const el = targetRef.current;
      const parentPopover = parentContext.current;

      if (!el || !onRequestClose || !isMountedAndOpenRef.current) return;
      // if click inside parent popover then close
      const isInsideParentPopover = parentPopover && isInside(e, parentPopover);
      const isInsidePopover = isInsideRole(e, 'popover');
      const isInDOM = document.contains(e.target as HTMLElement);

      // CLose popover if el is DOM and event is not inside element itself or popover, or event is in parent popover
      if (!isInside(e, el) && isInDOM && (isInsideParentPopover || !isInsidePopover)) {
        onRequestClose(e);
      }
    },
    disabledClickOutside || hide || !onRequestClose,
  );

  const onEscape = useCallback(() => {
    if (!hide && typeof onRequestClose === 'function') {
      onRequestClose();
    }
  }, [hide, onRequestClose]);

  useKeyUp('Escape', onEscape, [hide]);

  return createPortal(
    <PopoverWrapperContext.Provider value={targetRef}>{children}</PopoverWrapperContext.Provider>,
    targetRef.current as HTMLElement,
  );
}

export function isRefObject(value?: PopoverAnchor): value is React.RefObject<HTMLElement> {
  return typeof value !== 'undefined' && 'current' in value;
}

function getOriginOffset(origin: PopoverOrigin): [x: number, y: number] {
  const [vertical, horizontal] = origin.split('-');
  return [OriginMap[horizontal], OriginMap[vertical]];
}

export function applyPopoverStyles(
  target: HTMLElement,
  anchor: PopoverAnchor = [window.innerWidth / 2, window.innerHeight / 2],
  anchorOrigin: PopoverOrigin = 'center-center',
  targetOrigin: PopoverOrigin = 'center-center',
  offsetX = 0,
  offsetY = 0,
  zIndex?: number,
  matchAnchorWidth?: boolean,
  matchAnchorHeight?: boolean,
  pointerEventsNone?: boolean | undefined,
  translateAnimation?: boolean,
): void {
  let width: string | null = null;
  let height: string | null = null;
  if (isRefObject(anchor)) {
    if (anchor.current) {
      const anchorRect = anchor.current.getBoundingClientRect();
      const [originX, originY] = getOriginOffset(anchorOrigin);
      anchor = [
        anchorRect.left + offsetX + anchorRect.width * originX,
        anchorRect.top + offsetY + anchorRect.height * originY,
      ];

      if (matchAnchorWidth) width = anchorRect.width + 'px';
      if (matchAnchorHeight) height = anchorRect.height + 'px';
    } else {
      anchor = [0, 0];
    }
  } else if (Array.isArray(anchor)) anchor = anchor.slice() as [number, number];

  const targetRect = target.getBoundingClientRect();
  const [originX, originY] = getOriginOffset(targetOrigin);
  const maxX = window.innerWidth - targetRect.width;
  const maxY = window.innerHeight - targetRect.height;
  anchor[0] = clamp(anchor[0] + offsetX - targetRect.width * originX, 0, maxX);
  anchor[1] = clamp(anchor[1] + offsetY - targetRect.height * originY, 0, maxY);
  if (translateAnimation) target.style.transition = 'all 0.1s ease-out';
  Object.assign(target.style, {
    left: anchor[0] + 'px',
    top: anchor[1] + 'px',
    width,
    height,
    ...(pointerEventsNone && { pointerEvents: 'none' }),
    ...(targetRect.height >= window.innerHeight && { overflow: 'auto' }),
  });
  if (zIndex) target.style.zIndex = zIndex.toString();
}
