import { useWindowSize } from "@solid-primitives/resize-observer";
import { MenuList } from "@suid/material";
import {
  batch,
  createEffect,
  createSignal,
  splitProps,
  type Component,
  type JSX,
  type ParentProps,
} from "solid-js";
import { ANIM_CURVE_STD } from "./theme";
import "./Menu.css";
import {
  animateGrowFromTopRight,
  animateShrinkToTopRight,
} from "~platform/anim";
import type { MenuListProps } from "@suid/material/MenuList";

export type Anchor = Pick<DOMRect, "top" | "left" | "right"> & { e?: number };

export type MenuProps = ParentProps<
  {
    open?: boolean;
    onClose?: JSX.EventHandlerUnion<HTMLDialogElement, Event>;
    anchor: () => Anchor;
    MenuListProps?: MenuListProps;

    id?: string;
  } & JSX.AriaAttributes
>;

function px(n?: number) {
  if (n) {
    return `${n}px`;
  } else {
    return undefined;
  }
}

/**
 * Create managed state for {@link Menu}. This function
 * expose an "open" closure for you to open the menu. The
 * opening and closing is automatically managed internally.
 *
 * @returns The first element is the "open" closure, calls
 *   with anchor infomation to open the menu.
 *   The second element is the state props for {@link Menu}, use
 *   spread syntax to set the props.
 * @example
 * ````tsx
 * const [openMenu, menuState] = createManagedMenuState();
 *
 * <Menu {...menuState}></Menu>
 *
 * <Button onClick={event => openMenu(event.currectTarget.getBoundingClientRect())} />
 * ````
 */
export function createManagedMenuState() {
  const [anchor, setAnchor] = createSignal<Anchor>();

  return [
    setAnchor,
    {
      get open() {
        return !!anchor();
      },
      anchor: anchor as () => Anchor,
      onClose: (event: Event) => {
        event.preventDefault();
        return setAnchor();
      },
    },
  ] as const;
}

function animateGrowFromTopLeft(
  element: HTMLElement,
  opts?: Omit<KeyframeAnimationOptions, "duration">,
) {
  const rend = element.getBoundingClientRect();
  const overflow = element.style.overflow;
  element.style.overflow = "hidden";
  const duration = (rend.height / 1600) * 1000;
  const animation = element.animate(
    {
      height: [`${rend.height / 2}px`, `${rend.height}px`],
      width: [`${(rend.width / 4) * 3}px`, `${rend.width}px`],
    },
    {
      duration,
      ...opts,
    },
  );
  animation.addEventListener(
    "finish",
    () => (element.style.overflow = overflow),
  );
  return animation;
}

/**
 * Material Menu Component. This component is
 * implemented with dialog and {@link MenuList} from SUID.
 *
 * Notes:
 * - Use {@link createManagedMenuState} and you don't need to manage the open and close.
 * - Use {@link MenuItem} from SUID as children.
 */
const Menu: Component<MenuProps> = (oprops) => {
  let root: HTMLDialogElement;
  const windowSize = useWindowSize();
  const [props, rest] = splitProps(oprops, [
    "open",
    "onClose",
    "anchor",
    "MenuListProps",
    "children",
  ]);

  const [anchorPos, setAnchorPos] = createSignal<{
    left?: number;
    top?: number;
    e?: number;
  }>({});

  if (import.meta.env.DEV) {
    createEffect(() => {
      if (anchorPos().e)
        switch (anchorPos().e) {
          case 1:
          case 2:
          case 3:
          case 4:
            return;
          default:
            console.warn('value %s is invalid for param "e"', anchorPos().e);
        }
    });
  }

  let openAnimationOrigin: "lt" | "rt" = "lt";

  const animateOpen = () => {
    const a = props.anchor();
    const { width } = windowSize;
    const { left, top, right, e } = a;
    const isOpened = root.open;

    // There are incomplete animations.
    // For `getBoundingClientRect()`, WebKit reports the initial state
    // of the element, whilst Firefox reports the final state.
    //
    // We skip if animations are still on the element
    // to avoid the problem on WebKit.
    // Here use the final state.
    //
    // This is a dirty workaround. It's here because the feature is still
    // works with it.
    // I am curious that why the ones on the other parts are works. (Rubicon)
    if (root.getAnimations().length > 0) {
      return;
    }

    root.showModal();
    const rend = root.getBoundingClientRect();

    if (left > width / 2) {
      openAnimationOrigin = "rt";
      setAnchorPos({
        left: right - rend.width,
        top,
        e,
      });
    } else {
      openAnimationOrigin = "lt";
      setAnchorPos({ left, top, e });
    }

    if (!isOpened) {
      switch (openAnimationOrigin) {
        case "lt":
          animateGrowFromTopLeft(root, { easing: ANIM_CURVE_STD });
          break;
        case "rt":
          animateGrowFromTopRight(root, { easing: ANIM_CURVE_STD });
          break;
      }
    }
  };

  createEffect(() => {
    if (props.open) {
      animateOpen();
    } else {
      animateClose();
    }
  });

  const animateClose = () => {
    const rend = root.getBoundingClientRect();
    if (openAnimationOrigin === "lt") {
      const overflow = root.style.overflow;
      root.style.overflow = "hidden";
      const animation = root.animate(
        {
          height: [`${rend.height}px`, `${rend.height / 2}px`],
          width: [`${rend.width}px`, `${(rend.width / 4) * 3}px`],
        },
        {
          duration: (rend.height / 2 / 1600) * 1000,
          easing: ANIM_CURVE_STD,
        },
      );
      animation.addEventListener("finish", () => {
        root.style.overflow = overflow;
        root.close();
      });
    } else {
      const animation = animateShrinkToTopRight(root, {
        easing: ANIM_CURVE_STD,
      });
      animation.addEventListener("finish", () => {
        root.close();
      });
    }
  };

  const onDialogClick = (
    event: MouseEvent & { currentTarget: HTMLDialogElement },
  ) => {
    event.stopPropagation();
    if (event.currentTarget !== event.target) return;
    if (!event.currentTarget.open) return;

    const rect = event.currentTarget.getBoundingClientRect();
    const isNotInDialog =
      event.clientY < rect.top ||
      event.clientY > rect.bottom ||
      event.clientX < rect.left ||
      event.clientX > rect.right;

    if (isNotInDialog) {
      if (props.onClose) {
        if (Array.isArray(props.onClose)) {
          props.onClose[0](props.onClose[1], event);
        } else {
          (
            props.onClose as (
              event: Event & { currentTarget: HTMLDialogElement },
            ) => void
          )(event);
        }
      }
    }
  };

  return (
    <dialog
      ref={root!}
      onClose={props.onClose}
      onCancel={props.onClose}
      onClick={onDialogClick}
      class={`Menu e${anchorPos().e || "0"}`}
      style={{
        left: px(anchorPos().left),
        top: px(anchorPos().top),
        /* FIXME: the content may be overflow */
      }}
      role="presentation"
      tabIndex={-1}
      {...rest}
    >
      <div class="container" role="presentation">
        <MenuList {...props.MenuListProps}>{props.children}</MenuList>
      </div>
    </dialog>
  );
};

export default Menu;
