import {
  createEffect,
  createSignal,
  Show,
  untrack,
  type Component,
} from "solid-js";
import { css } from "solid-styled";
import { Refresh as RefreshIcon } from "@suid/icons-material";
import { CircularProgress } from "@suid/material";
import { makeEventListener } from "@solid-primitives/event-listener";
import { createVisibilityObserver } from "@solid-primitives/intersection-observer";
import { useMaybeIsFrameSuspended } from "../platform/StackedRouter";

const PullDownToRefresh: Component<{
  loading?: boolean;
  linkedElement?: HTMLElement;
  onRefresh?: () => void;
}> = (props) => {
  let rootElement: HTMLDivElement;
  const [pullDown, setPullDown] = createSignal(0);

  const stopPos = () => 160;

  const indicatorOfsY = () => {
    if (props.loading) {
      return stopPos() * 0.875;
    }
    return pullDown();
  };

  const obvx = createVisibilityObserver({
    threshold: 0.0001,
  });

  const rootVisible = obvx(() => rootElement);
  const isFrameSuspended = useMaybeIsFrameSuspended()

  createEffect(() => {
    if (!rootVisible()) setPullDown(0);
  });

  let released = true;
  let v = 0;
  let lts = -1;
  let ds = 0;
  let holding = false;
  const K = 20;
  const updatePullDown = (ts: number) => {
    released = false;
    try {
      const x = untrack(pullDown);
      const dt = lts !== -1 ? ts - lts : 1 / 60;
      const vspring = holding ? 0 : K * x * dt;
      v = ds / dt - vspring;
      const final = Math.max(Math.min(x + v * dt, stopPos()), 0);

      setPullDown(final);

      if (Math.abs(x) > 1 || Math.abs(v) > 1) {
        requestAnimationFrame(updatePullDown);
      } else {
        v = 0;
        lts = -1;
      }
    } finally {
      ds = 0;
      released = true;
    }
  };

  let wheelTimeout: ReturnType<typeof setTimeout> | undefined;

  const onWheelNotUpdated = () => {
    wheelTimeout = undefined;
    holding = false;

    if (released) {
      released = false;
      requestAnimationFrame(updatePullDown);
    }
  };

  const handleLinkedWheel = (event: WheelEvent) => {
    const scrollTop = (event.target as HTMLElement).scrollTop;
    if (scrollTop >= 0 && scrollTop < 1) {
      const d = untrack(pullDown);
      if (d > 1) event.preventDefault();
      ds = -(event.deltaY / window.devicePixelRatio / 2);
      if (wheelTimeout) {
        clearTimeout(wheelTimeout);
      }
      if (d >= stopPos() && !props.loading) {
        props.onRefresh?.();

        holding = false;
        wheelTimeout = undefined;
      } else {
        holding = true;
        wheelTimeout = setTimeout(onWheelNotUpdated, 200);
      }

      if (released) {
        released = false;
        requestAnimationFrame(updatePullDown);
      }
    }
  };

  createEffect(() => {
    if (!rootVisible()) {
      return;
    }
    if (isFrameSuspended()) {
      return;
    }
    const element = props.linkedElement;
    if (!element) return;
    makeEventListener(element, "wheel", handleLinkedWheel);
  });

  let lastTouchId: number | undefined = undefined;
  let lastTouchScreenY = 0;
  const handleTouch = (event: TouchEvent) => {
    if (event.targetTouches.length > 1) {
      lastTouchId = 0;
      lastTouchScreenY = 0;
      return;
    }
    const item = event.targetTouches.item(0)!;
    if (lastTouchId && item.identifier !== lastTouchId) {
      lastTouchId = undefined;
      lastTouchScreenY = 0;
      return;
    }

    holding = true;
    if (lastTouchScreenY !== 0) {
      ds = item.screenY - lastTouchScreenY;
    }
    if (Math.abs(ds) > 1 && untrack(pullDown) > 1) event.preventDefault();
    lastTouchScreenY = item.screenY;
    if (released) {
      released = false;
      requestAnimationFrame(updatePullDown);
    }
  };

  const handleTouchEnd = () => {
    lastTouchId = undefined;
    lastTouchScreenY = 0;
    holding = false;
    if (untrack(pullDown) >= stopPos() && !props.loading) {
      props.onRefresh?.();
    } else {
      if (released) {
        released = false;
        requestAnimationFrame(updatePullDown);
      }
    }
  };

  createEffect(() => {
    if (!rootVisible()) {
      return;
    }
    if (isFrameSuspended()) {
      return;
    }
    const element = props.linkedElement;
    if (!element) return;
    makeEventListener(element, "touchmove", handleTouch);
    makeEventListener(element, "touchend", handleTouchEnd);
  });

  css`
    .pull-down {
      width: 100%;
      display: flex;
      justify-content: center;
      margin-top: -2rem;
      height: calc(1px + 2rem);
    }

    .indicator {
      display: inline-flex;
      justify-content: center;
      align-items: center;
      box-shadow: ${props.loading
        ? "var(--tutu-shadow-e12)"
        : "var(--tutu-shadow-e1)"};
      border-radius: 50%;
      aspect-ratio: 1/1;
      width: 2rem;
      color: var(--tutu-color-primary);
      transform: translateY(${`${indicatorOfsY() - 2}px`});
      will-change: transform;
      z-index: var(--tutu-zidx-nav);
      background-color: var(--tutu-color-surface);

      > :global(.refresh-icon) {
        transform: rotate(${`${(indicatorOfsY() / 160 / 2).toString()}turn`});
        will-change: transform;
      }

      > :global(.refresh-indicator) {
        width: 1.5rem;
        height: 1.5rem;
        aspect-ratio: 1/1;
      }
    }
  `;
  return (
    <div ref={rootElement!} class="pull-down">
      <span class="indicator">
        <Show
          when={props.loading}
          fallback={<RefreshIcon class="refresh-icon" />}
        >
          <CircularProgress class="refresh-indicator" />
        </Show>
      </span>
    </div>
  );
};

export default PullDownToRefresh;
