// Desktop / tablet media viewer — a free-floating, draggable window that
// renders ON TOP of the app instead of the old right-side flex pane. It
// reuses `MediaViewerBody` (resolve/decrypt, zoom, pan, swipe, prev/next)
// with `hideHeader` so the only chrome is a thin drag bar + close button,
// plus a subtle corner resize grip. Portaled to #portalContainer so no
// ancestor transform/overflow can clip it.
//
// Mounted in `Room.tsx` for non-mobile screens; mobile uses
// `MobileMediaViewerHorseshoe`.

import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { useAtom, useAtomValue } from 'jotai';
import { useTranslation } from 'react-i18next';
import { Icon, IconButton, Icons } from 'folds';
import { mediaViewerAtom } from '../../state/mediaViewer';
import { useCloseMediaViewer } from '../../state/hooks/mediaViewer';
import { useRoom } from '../../hooks/useRoom';
import {
  MediaWindowRect,
  clampMediaWindowRect,
  defaultMediaWindowRect,
  mediaWindowRectAtom,
} from '../../state/mediaWindowRect';
import { MediaViewerBody } from './MediaViewerBody';
import * as css from './RoomViewMediaWindow.css';

type GestureMode = 'move' | 'resize';

export function RoomViewMediaWindow() {
  const { t } = useTranslation();
  const entry = useAtomValue(mediaViewerAtom);
  const close = useCloseMediaViewer();
  const room = useRoom();
  const [savedRect, setSavedRect] = useAtom(mediaWindowRectAtom);

  const open = !!entry;

  const [viewport, setViewport] = useState(() => ({
    w: typeof window === 'undefined' ? 1280 : window.innerWidth,
    h: typeof window === 'undefined' ? 800 : window.innerHeight,
  }));
  useEffect(() => {
    const onResize = () => setViewport({ w: window.innerWidth, h: window.innerHeight });
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, []);

  // Live window geometry. Seeded from the saved rect (clamped) or a
  // centred default the first time the window opens, then mutated by
  // drag / resize and re-clamped on viewport resize.
  const [rect, setRect] = useState<MediaWindowRect | null>(null);

  const wasOpenRef = useRef(false);
  useLayoutEffect(() => {
    if (open && !wasOpenRef.current) {
      const vw = window.innerWidth;
      const vh = window.innerHeight;
      // Route BOTH branches through the clamp so the centred default is
      // floored to the min size too (defaultMediaWindowRect alone can dip
      // below MEDIA_WINDOW_MIN_H on a very short viewport).
      setRect(clampMediaWindowRect(savedRect ?? defaultMediaWindowRect(vw, vh), vw, vh));
    }
    wasOpenRef.current = open;
  }, [open, savedRect]);

  // Re-clamp into the viewport when it shrinks while the window is open.
  useEffect(() => {
    if (!open) return;
    setRect((prev) => (prev ? clampMediaWindowRect(prev, viewport.w, viewport.h) : prev));
  }, [open, viewport.w, viewport.h]);

  // Drag (move) + resize share one gesture, distinguished by `mode`.
  // We listen on `document` (added on pointerdown, removed on pointerup)
  // so the gesture keeps tracking even when the cursor leaves the small
  // drag bar / grip. `startRect` is snapshotted so each move computes an
  // absolute delta from the gesture origin (no drift accumulation).
  const gestureRef = useRef<{
    mode: GestureMode;
    startX: number;
    startY: number;
    startRect: MediaWindowRect;
  } | null>(null);

  const persist = useCallback(
    (r: MediaWindowRect) => {
      setSavedRect(clampMediaWindowRect(r, window.innerWidth, window.innerHeight));
    },
    [setSavedRect]
  );

  const onGesturePointerMove = useCallback((e: PointerEvent) => {
    const g = gestureRef.current;
    if (!g) return;
    e.preventDefault();
    const dx = e.clientX - g.startX;
    const dy = e.clientY - g.startY;
    const vw = window.innerWidth;
    const vh = window.innerHeight;
    if (g.mode === 'move') {
      setRect(
        clampMediaWindowRect(
          { ...g.startRect, x: g.startRect.x + dx, y: g.startRect.y + dy },
          vw,
          vh
        )
      );
    } else {
      // Resize from the bottom-right grip (x/y fixed): cap growth to the
      // space available to the right / below the window's origin so the
      // grip can't be pushed off-screen and become unreachable.
      setRect(
        clampMediaWindowRect(
          {
            ...g.startRect,
            w: Math.min(g.startRect.w + dx, vw - g.startRect.x),
            h: Math.min(g.startRect.h + dy, vh - g.startRect.y),
          },
          vw,
          vh
        )
      );
    }
  }, []);

  const onGesturePointerUp = useCallback(() => {
    gestureRef.current = null;
    document.removeEventListener('pointermove', onGesturePointerMove);
    document.removeEventListener('pointerup', onGesturePointerUp);
    document.removeEventListener('pointercancel', onGesturePointerUp);
    setRect((prev) => {
      if (prev) persist(prev);
      return prev;
    });
  }, [onGesturePointerMove, persist]);

  const beginGesture = useCallback(
    (mode: GestureMode, e: React.PointerEvent<HTMLDivElement>) => {
      if (!rect || e.button !== 0) return;
      e.preventDefault();
      gestureRef.current = { mode, startX: e.clientX, startY: e.clientY, startRect: rect };
      document.addEventListener('pointermove', onGesturePointerMove);
      document.addEventListener('pointerup', onGesturePointerUp);
      // A cancelled pointer (tablet edge-gesture, palm rejection, OS
      // scroll takeover) fires pointercancel instead of pointerup — end
      // the gesture the same way so it can't strand + leak listeners.
      document.addEventListener('pointercancel', onGesturePointerUp);
    },
    [rect, onGesturePointerMove, onGesturePointerUp]
  );

  // Belt-and-suspenders: drop any in-flight document listeners if the
  // window unmounts mid-drag (route change, atom cleared).
  useEffect(
    () => () => {
      document.removeEventListener('pointermove', onGesturePointerMove);
      document.removeEventListener('pointerup', onGesturePointerUp);
      document.removeEventListener('pointercancel', onGesturePointerUp);
    },
    [onGesturePointerMove, onGesturePointerUp]
  );

  // Esc closes (the body owns arrows / +/- / 0; it deliberately leaves
  // Esc to the host so each shell can decide its own dismiss policy).
  const closeRef = useRef(close);
  closeRef.current = close;
  const entryRef = useRef(entry);
  entryRef.current = entry;

  // Clear the global media atom when the room changes (this window lives
  // under a `key={room.roomId}` RoomProvider, so a room switch unmounts
  // it). Without this the next room would re-seed the window and float
  // the previous room's media over it with dead prev/next (currentIndex
  // = -1). Mirrors RoomViewProfileSidePanel + the mobile horseshoe. Refs
  // so the cleanup doesn't re-fire on every prev/next entry step.
  useEffect(
    () => () => {
      if (entryRef.current) closeRef.current();
    },
    [room.roomId]
  );

  useEffect(() => {
    if (!open) return undefined;
    const onKeyDown = (e: KeyboardEvent) => {
      if (e.key !== 'Escape') return;
      const target = e.target as HTMLElement | null;
      if (
        target &&
        (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)
      ) {
        return;
      }
      closeRef.current();
    };
    window.addEventListener('keydown', onKeyDown);
    return () => window.removeEventListener('keydown', onKeyDown);
  }, [open]);

  if (!open || !entry || !rect) return null;

  const portalTarget =
    typeof document !== 'undefined'
      ? document.getElementById('portalContainer') ?? document.body
      : null;
  if (!portalTarget) return null;

  return createPortal(
    <div
      className={css.floating}
      style={{ left: rect.x, top: rect.y, width: rect.w, height: rect.h }}
      role="dialog"
      aria-label={entry.body || t('MediaViewer.title', 'Media viewer')}
    >
      {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
      <div className={css.dragBar} onPointerDown={(e) => beginGesture('move', e)}>
        <span className={css.title}>{entry.body}</span>
        <IconButton
          variant="Background"
          fill="None"
          radii="300"
          size="300"
          // Don't let a click on the close button start a window drag.
          onPointerDown={(e: React.PointerEvent) => e.stopPropagation()}
          onClick={close}
          aria-label={t('Common.close', 'Close')}
        >
          <Icon size="100" src={Icons.Cross} />
        </IconButton>
      </div>
      <div className={css.body}>
        <MediaViewerBody entry={entry} requestClose={close} hideHeader />
      </div>
      {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
      <div
        className={css.resizeGrip}
        aria-hidden="true"
        onPointerDown={(e) => beginGesture('resize', e)}
      />
    </div>,
    portalTarget
  );
}
