import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useAtomValue, useSetAtom } from 'jotai';
import { isNativePlatform } from '../../utils/capacitor';
import {
  MobilePagerMascotApi,
  mobileHorseshoeActiveAtom,
  mobilePagerAnyRevealAtom,
  mobilePagerFormMountedAtom,
  mobilePagerMascotApiAtom,
} from '../../state/mobilePagerHeader';
import { RefreshMascot } from '../stream-header/RefreshMascot';
import { RadarPulse, useEchoes } from '../stream-header/RadarPulse';
import { CURTAIN_SNAP_MS } from '../stream-header/geometry';
import * as css from './style.css';

type PagerRefreshSingletonProps = {
  // True while the strip is moving horizontally — a finger drag, the
  // commit-slide, or the spring-back settle. Used ONLY to suppress the
  // post-close card linger (see `cardLinger` below): the lingering card
  // would otherwise sweep past the transparent inter-pane gap as a
  // visible sliver. While a tab IS revealing, the card intentionally
  // stays through a swipe — that's the whole «mascot doesn't ride the
  // panes» point.
  pagerAnimating: boolean;
  // The strip's live X, NEGATED (computed by MobileTabsPager next to
  // the strip transform). Applied as a direct inline `transform` on the
  // promoted host so the counter-translation is committed in the SAME
  // React style flush as the strip's transform and — during the 280ms
  // commit-slide — transitions on the COMPOSITOR in lockstep with it.
  // (A registered-@property var was tried first: custom-property
  // interpolation runs on the main thread, and the navigate() commit at
  // slide start froze it for the first frames of every swipe commit —
  // the mascot visibly rode the strip, then snapped back.)
  counterTx: string;
  // The strip's transition string, verbatim (`none` while dragging /
  // instant-switching, the 280ms slide otherwise) — single source so
  // the two transforms can never desync.
  transition: string;
};

// The pull-to-refresh chrome singleton — renders the dancing mascot +
// radar-pulse rings ONCE as the strip's first child, counter-translated
// against the strip's live translation so it stays SCREEN-static while
// the panes slide (see `style.css.ts::pagerRefreshSingleton` for the
// hosting / tile-memory / paint-order contract). The pull-to-refresh is
// an app-wide /sync poke, and the user reads the mascot as belonging to
// the app, not to one tab. The per-pane StreamHeaders render no mascot
// of their own in pager mode; each pane publishes its reveal state into
// `curtainRefreshRevealByTabAtom` and this singleton paints while ANY
// tab is revealing (an inactive tab's curtain legitimately rests at
// `refresh` through its auto-close dwell — keeping the chrome alive for
// it makes a swipe-away AND a return swipe mid-refresh seamless, no
// dance restart).
//
// Memoised: the parent pager re-renders on every horizontal-drag frame;
// the single primitive prop plus boolean-derived atoms keep this
// component to actual flips.
export const PagerRefreshSingleton = React.memo(
  ({ pagerAnimating, counterTx, transition }: PagerRefreshSingletonProps) => {
    const { t } = useTranslation();
    const anyRevealing = useAtomValue(mobilePagerAnyRevealAtom);

    // Hide while the ACTIVE pane's inline search form is MOUNTED (not
    // just while its snap is a form snap): this singleton paints behind
    // the panes' content, and the form band has no opaque backdrop in
    // pager mode — a mascot kept alive by ANOTHER tab's in-flight
    // refresh (or by the linger) would show through the still-visible
    // CLOSING form during its 280–480ms slide-up window.
    const formMounted = useAtomValue(mobilePagerFormMountedAtom);

    // Hide while a horseshoe sheet is geometrically active. The sheet
    // flips its pane-covering appBody OPAQUE on the first frame of drag
    // (void-containment contract — see MobileSettingsHorseshoe), and
    // appBody is a positioned later-in-tree sibling inside the pane,
    // i.e. it paints over this singleton anyway. Unmounting on the same
    // signal makes the hand-off explicit and stops the dance <video>
    // decoding behind an opaque surface. Pre-singleton, the in-pane
    // card stayed visible above appBody during a sheet drag; that
    // nicety is consciously traded away — the overlap (sheet opened
    // during the ≤8s refresh dwell) is rare, cosmetic, and
    // self-corrects when the refresh auto-closes under the sheet.
    const horseshoeActive = useAtomValue(mobileHorseshoeActiveAtom);

    const revealing = anyRevealing && !formMounted && !horseshoeActive;

    // The dance <video> while mounted (null while the reduced-motion /
    // resting poster is shown). Handed to the pane's StreamHeader
    // through the api atom so its auto-close can align to a dance-loop
    // boundary — same contract as the in-stage `mascotVideoRef`.
    const videoRef = useRef<HTMLVideoElement>(null);

    // Echo state lives here (not in the per-pane StreamHeaders) so the
    // one-shot rings share the singleton's canvas. NB an echo spawned
    // just before close survives ~4.1s in this state; on a quick
    // re-reveal it RESUMES mid-pass (echo phases are absolute
    // `performance.now()` timestamps — see `EchoPulse`), it doesn't
    // replay from zero like the old CSS-animation circles did.
    const { echoes, spawnEcho } = useEchoes();

    // Read once at mount, mirroring `RefreshMascot` / `StreamHeader` —
    // this singleton is native-only + aria-hidden decoration, so a
    // mid-session OS toggle not re-evaluating here is acceptable.
    const reducedMotion = useMemo(
      () =>
        typeof window !== 'undefined' &&
        !!window.matchMedia &&
        window.matchMedia('(prefers-reduced-motion: reduce)').matches,
      []
    );

    // Publish the imperative channel (stable members → stable object, so
    // the StreamHeader effects that consume it don't re-run spuriously).
    // Compare-and-clear on unmount, mirroring `mobilePagerCurtainAtom`.
    const api = useMemo<MobilePagerMascotApi>(
      () => ({ getVideo: () => videoRef.current, spawnEcho }),
      [spawnEcho]
    );
    const setApi = useSetAtom(mobilePagerMascotApiAtom);
    useEffect(() => {
      setApi(api);
      return () => {
        setApi((prev) => (prev === api ? null : prev));
      };
    }, [api, setApi]);

    // Keep the mascot CARD mounted through the curtain's close animation.
    // `revealing` flips false the instant the snap commits to `closed`,
    // but the curtain takes CURTAIN_SNAP_MS to slide up over the band —
    // unmounting the card immediately would visibly vanish the mascot
    // before the curtain covers it. The in-stage variant never had this
    // problem (its card is permanently mounted in the header); here the
    // card must NOT be permanently visible, or it would peek through the
    // inter-pane gap during plain tab swipes at rest. For the same
    // reason the linger itself is suppressed (a) while the strip is
    // moving (`pagerAnimating` — the gap would sweep a sliver of the
    // card across the screen) and (b) under the form / horseshoe gates
    // above, where `revealing` is forced false while the band isn't
    // actually closing. The rings don't linger — parity with the
    // in-stage layer, which unmounts the pulse at close-commit. During
    // the linger the card renders `revealing=false`, i.e. the still
    // poster.
    const [cardLinger, setCardLinger] = useState(false);
    useEffect(() => {
      if (revealing) {
        setCardLinger(true);
        return undefined;
      }
      if (!cardLinger) return undefined;
      const timer = window.setTimeout(() => setCardLinger(false), CURTAIN_SNAP_MS + 120);
      return () => window.clearTimeout(timer);
    }, [revealing, cardLinger]);
    const showCard =
      revealing || (cardLinger && !pagerAnimating && !formMounted && !horseshoeActive);
    const showRings = revealing && !reducedMotion;

    // The pager only mounts on mobile + native, so this is belt-and-
    // suspenders (and keeps the hooks above unconditional).
    if (!isNativePlatform()) return null;

    // Zero DOM / zero layers during plain swipes — nothing can peek
    // through the transparent inter-pane gap.
    if (!showCard && !showRings) return null;

    return (
      <div
        className={css.pagerRefreshSingleton}
        // `translateZ(0)` promotes the host so the transition above runs
        // compositor-side; the layer itself is cheap (the host paints
        // nothing — tiles materialise only for the card band; the rings
        // canvas is its own untiled TextureLayer, see RadarPulse.tsx).
        style={{ transform: `translateX(${counterTx}) translateZ(0)`, transition }}
        aria-hidden="true"
      >
        {showCard && (
          <div className={css.pagerMascotSlot}>
            <RefreshMascot
              caption={t('Direct.refreshing')}
              revealing={revealing}
              videoRef={videoRef}
            />
          </div>
        )}
        {/* Rings paint AFTER (= over) the mascot card, matching the
            in-stage stacking where the pulse layer sits above the header
            that hosts the card. */}
        {showRings && <RadarPulse echoes={echoes} />}
      </div>
    );
  }
);
PagerRefreshSingleton.displayName = 'PagerRefreshSingleton';
