import {
  children,
  createEffect,
  createSignal,
  onCleanup,
  useTransition,
  type ParentComponent,
  type ResolvedChildren,
} from "solid-js";
import "./BottomSheet.css";
import { useHeroSignal } from "../platform/anim";
import material from "./material.module.css";

export type BottomSheetProps = {
  open?: boolean;
  bottomUp?: boolean;
  onClose?(reason: "backdrop"): void;
};

export const HERO = Symbol("BottomSheet Hero Symbol");

function composeAnimationFrame(
  {
    top,
    left,
    height,
    width,
  }: Record<"top" | "left" | "height" | "width", number>,
  x: Record<string, unknown>,
) {
  return {
    top: `${top}px`,
    left: `${left}px`,
    height: `${height}px`,
    width: `${width}px`,
    ...x,
  };
}

const MOVE_SPEED = 1200;

const BottomSheet: ParentComponent<BottomSheetProps> = (props) => {
  let element: HTMLDialogElement;
  let animation: Animation | undefined;
  const [hero, setHero] = useHeroSignal(HERO);
  const [cache, setCache] = createSignal<ResolvedChildren | undefined>();
  const ochildren = children(() => props.children);

  const [pending] = useTransition();

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

  const onClose = () => {
    const srcElement = hero();
    if (srcElement) {
      srcElement.style.visibility = "unset";
    }

    element.close();
    setHero();
  };

  const animatedClose = () => {
    const srcElement = hero();
    const endRect = srcElement?.getBoundingClientRect();
    if (endRect) {
      const startRect = element.getBoundingClientRect();
      const animation = animateHero(startRect, endRect, element, true);
      animation.addEventListener("finish", onClose);
      animation.addEventListener("cancel", onClose);
    } else {
      if (window.innerWidth > 560 && !props.bottomUp) {
        onClose();
        return;
      }
      const animation = props.bottomUp
        ? animateSlideInFromBottom(element, true)
        : animateSlideInFromRight(element, true);
      animation.addEventListener("finish", onClose);
      animation.addEventListener("cancel", onClose);
    }
  };

  const animatedOpen = () => {
    element.showModal();
    const srcElement = hero();
    const startRect = srcElement?.getBoundingClientRect();
    if (!startRect) {
      console.debug("no source element");
    }
    if (startRect) {
      srcElement!.style.visibility = "hidden";
      const endRect = element.getBoundingClientRect();
      animateHero(startRect, endRect, element);
    } else if (props.bottomUp) {
      animateSlideInFromBottom(element);
    } else if (window.innerWidth <= 560) {
      animateSlideInFromRight(element);
    }
  };

  const animateSlideInFromRight = (element: HTMLElement, reserve?: 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.left - window.innerWidth);
    const duration = (distance / MOVE_SPEED) * 1000;

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

  const animateSlideInFromBottom = (
    element: HTMLElement,
    reserve?: 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;

    animation = element.animate(
      {
        top: reserve
          ? [`${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 = undefined;
    };
    animation.addEventListener("cancel", onAnimationEnd);
    animation.addEventListener("finish", onAnimationEnd);
    return animation;
  };

  const animateHero = (
    startRect: DOMRect,
    endRect: DOMRect,
    element: HTMLElement,
    reserve?: boolean,
  ) => {
    const easing = "cubic-bezier(0.4, 0, 0.2, 1)";
    element.classList.add("animated");
    const distance = Math.sqrt(
      Math.pow(Math.abs(startRect.top - endRect.top), 2) +
        Math.pow(Math.abs(startRect.left - startRect.top), 2),
    );
    const duration = (distance / MOVE_SPEED) * 1000;
    animation = element.animate(
      [
        composeAnimationFrame(startRect, { transform: "none" }),
        composeAnimationFrame(endRect, { transform: "none" }),
      ],
      { easing, duration },
    );
    const onAnimationEnd = () => {
      element.classList.remove("animated");
      animation = undefined;
    };
    animation.addEventListener("finish", onAnimationEnd);
    animation.addEventListener("cancel", onAnimationEnd);
    return animation;
  };

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

  const onDialogClick = (
    event: MouseEvent & { currentTarget: HTMLDialogElement },
  ) => {
    const rect = event.currentTarget.getBoundingClientRect();
    const isInDialog =
      rect.top <= event.clientY &&
      event.clientY <= rect.top + rect.height &&
      rect.left <= event.clientX &&
      event.clientX <= rect.left + rect.width;
    if (!isInDialog) {
      props.onClose?.("backdrop");
    }
  };

  return (
    <dialog
      class={`BottomSheet ${material.surface}`}
      classList={{
        ["BottomSheet"]: true,
        [material.surface]: true,
        ["bottom"]: props.bottomUp,
      }}
      onClick={onDialogClick}
      ref={element!}
    >
      {ochildren() ?? cache()}
    </dialog>
  );
};

export default BottomSheet;
