import {
  children,
  createEffect,
  onCleanup,
  useTransition,
  type JSX,
  type ParentComponent,
} from "solid-js";
import "./BottomSheet.css";
import material from "./material.module.css";
import { ANIM_CURVE_ACELERATION, ANIM_CURVE_DECELERATION } from "./theme";
import {
  animateSlideInFromRight,
  animateSlideOutToRight,
} from "~platform/anim";

export type BottomSheetProps = {
  open?: boolean;
  bottomUp?: boolean;
  class?: JSX.HTMLAttributes<HTMLElement>["class"];
  onClose?(reason: "backdrop"): void;
};

const MOVE_SPEED = 1600;

function animateSlideInFromBottom(element: HTMLElement, reverse?: boolean) {
  const rect = element.getBoundingClientRect();
  const easing = "cubic-bezier(0.4, 0, 0.2, 1)";
  element.classList.add("animated");
  const oldOverflow = document.body.style.overflow;
  document.body.style.overflow = "hidden";
  const distance = Math.abs(rect.top - window.innerHeight);
  const duration = (distance / MOVE_SPEED) * 1000;

  const animation = element.animate(
    {
      top: reverse
        ? [`${rect.top}px`, `${window.innerHeight}px`]
        : [`${window.innerHeight}px`, `${rect.top}px`],
    },
    { easing, duration },
  );
  const onAnimationEnd = () => {
    element.classList.remove("animated");
    document.body.style.overflow = oldOverflow;
  };
  animation.addEventListener("cancel", onAnimationEnd);
  animation.addEventListener("finish", onAnimationEnd);
  return animation;
}

const BottomSheet: ParentComponent<BottomSheetProps> = (props) => {
  let element: HTMLDialogElement;
  let animation: Animation | undefined;
  const child = children(() => props.children);

  const [pending] = useTransition();

  createEffect(() => {
    if (props.open) {
      if (!element.open && !pending()) {
        requestAnimationFrame(animatedOpen);
      }
    } else {
      if (element.open) {
        animatedClose();
      }
    }
  });

  const onClose = () => {
    element.close();
  };

  const animatedClose = () => {
    if (window.innerWidth > 560 && !props.bottomUp) {
      onClose();
      return;
    }
    const onAnimationEnd = () => {
      element.classList.remove("animated");
      animation = undefined;
      onClose();
    };
    element.classList.add("animated");
    animation = props.bottomUp
      ? animateSlideInFromBottom(element, true)
      : animateSlideOutToRight(element, { easing: ANIM_CURVE_ACELERATION });
    animation.addEventListener("finish", onAnimationEnd);
    animation.addEventListener("cancel", onAnimationEnd);
  };

  const animatedOpen = () => {
    element.showModal();
    if (props.bottomUp) {
      animateSlideInFromBottom(element);
    } else if (window.innerWidth <= 560) {
      element.classList.add("animated");
      const onAnimationEnd = () => {
        element.classList.remove("animated");
        animation = undefined;
      };
      animation = animateSlideInFromRight(element, {
        easing: ANIM_CURVE_DECELERATION,
      });
      animation.addEventListener("finish", onAnimationEnd);
      animation.addEventListener("cancel", onAnimationEnd);
    }
  };

  onCleanup(() => {
    if (animation) {
      animation.cancel();
    }
  });

  const onDialogClick = (
    event: MouseEvent & { currentTarget: HTMLDialogElement },
  ) => {
    event.stopPropagation();
    if (event.target !== event.currentTarget) 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) {
      props.onClose?.("backdrop");
    }
  };

  const onDialogCancel = (event: Event) => {
    event.preventDefault();

    props.onClose?.("backdrop");
  };

  return (
    <dialog
      class={`BottomSheet ${material.surface} ${props.class || ""}`}
      classList={{
        ["bottom"]: props.bottomUp,
      }}
      onClick={onDialogClick}
      onCancel={onDialogCancel}
      ref={element!}
      tabIndex={-1}
      role="presentation"
    >
      {child()}
    </dialog>
  );
};

export default BottomSheet;
