/* eslint-disable jsx-a11y/media-has-caption */
import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import { useAtomValue, useSetAtom } from 'jotai';
import { MatrixRTCSession } from 'matrix-js-sdk/lib/matrixrtc/MatrixRTCSession';
import FocusTrap from 'focus-trap-react';
import {
  Avatar,
  Box,
  Button,
  color,
  config,
  Dialog,
  Icon,
  Icons,
  Overlay,
  OverlayBackdrop,
  OverlayCenter,
  Text,
  toRem,
} from 'folds';
import {
  EventTimelineSetHandlerMap,
  EventType,
  RelationType,
  Room,
  RoomEvent,
} from 'matrix-js-sdk';
import { IRTCNotificationContent, RTCNotificationType } from 'matrix-js-sdk/lib/matrixrtc/types';
import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend';
import {
  CallEmbedContextProvider,
  CallEmbedRefContextProvider,
  useCallHangupEvent,
  useCallJoined,
  useCallThemeSync,
  useCallMemberSoundSync,
  useCallStart,
} from '../hooks/useCallEmbed';
import { callChatAtom, callEmbedAtom } from '../state/callEmbed';
import { CallEmbed } from '../plugins/call';
import { useSelectedRoom } from '../hooks/router/useSelectedRoom';
import { ScreenSize, useScreenSizeContext } from '../hooks/useScreenSize';
import { useMatrixClient } from '../hooks/useMatrixClient';
import CallSound from '../../../public/sound/call.ogg';
import { useCallMembersChange, useCallSession } from '../hooks/useCall';
import { useRoomAvatar, useRoomName } from '../hooks/useRoomMeta';
import { mDirectAtom } from '../state/mDirectList';
import { useMediaAuthentication } from '../hooks/useMediaAuthentication';
import { mxcUrlToHttp } from '../utils/matrix';
import { RoomAvatar, RoomIcon } from './room-avatar';
import { useRoomNavigate } from '../hooks/useRoomNavigate';
import { getStateEvent, getMemberDisplayName } from '../utils/room';
import { getMxIdLocalPart } from '../utils/matrix';
import { StateEvent } from '../../types/matrix/room';
import { getPowersLevelFromMatrixEvent } from '../hooks/usePowerLevels';
import { getRoomCreatorsForRoomId } from '../hooks/useRoomCreators';
import { getRoomPermissionsAPI } from '../hooks/useRoomPermissions';
import { useLivekitSupport } from '../hooks/useLivekitSupport';
import { CallAvatarAnimation } from '../styles/Animations.css';
import { webRTCSupported } from '../utils/rtc';

const PIP_MIN_W = 200;
const PIP_MIN_H = 112;

type Corner = 'se' | 'sw' | 'ne' | 'nw';

/** Normalise the element to top/left positioning so resize math is uniform. */
function normaliseToTopLeft(el: HTMLElement) {
  const rect = el.getBoundingClientRect();
  el.style.left = `${rect.left}px`;
  el.style.top = `${rect.top}px`;
  el.style.width = `${rect.width}px`;
  el.style.height = `${rect.height}px`;
  el.style.right = 'auto';
  el.style.bottom = 'auto';
}

type IncomingCallInfo = {
  room: Room;
  sender: string;
  senderTs: number;
  lifetime: number;
  intent?: string;
  notificationType: RTCNotificationType;
  refEventId: string;
};
type IncomingCallProps = {
  dm: boolean;
  info: IncomingCallInfo;
  onIgnore: () => void;
  onAnswer: (room: Room, video: boolean) => void;
  onReject: (room: Room, eventId: string) => void;
};
function IncomingCall({ dm, info, onIgnore, onAnswer, onReject }: IncomingCallProps) {
  const mx = useMatrixClient();
  const useAuthentication = useMediaAuthentication();
  const livekitSupported = useLivekitSupport();
  const rtcSupported = webRTCSupported();
  const canAnswer = livekitSupported && rtcSupported;
  const { room } = info;

  const audioRef = useRef<HTMLAudioElement>(null);

  const roomName = useRoomName(room);
  const roomAvatar = useRoomAvatar(room, dm);
  const avatarUrl = roomAvatar
    ? (mxcUrlToHttp(mx, roomAvatar, useAuthentication, 96, 96, 'crop') ?? undefined)
    : undefined;

  const session = useCallSession(room);
  useCallMembersChange(
    session,
    useCallback(() => {
      const members = MatrixRTCSession.sessionMembershipsForRoom(room, session.sessionDescription);
      if (members.length === 0) {
        onIgnore();
      }
    }, [room, session, onIgnore]),
  );

  const playSound = useCallback(() => {
    const audioElement = audioRef.current;
    audioElement?.play().catch(() => undefined);
  }, []);

  useEffect(() => {
    if (info.notificationType === 'ring') {
      playSound();
    }
    return () => {
      if (audioRef.current) {
        audioRef.current.pause();
        audioRef.current.currentTime = 0;
      }
    };
  }, [playSound, info.notificationType]);

  useEffect(() => {
    const remaining = info.senderTs + info.lifetime - Date.now();
    if (remaining <= 0) {
      onIgnore();
      return;
    }
    const id = setTimeout(onIgnore, remaining);
    return () => clearTimeout(id);
  }, [info.senderTs, info.lifetime, onIgnore]);

  return (
    <>
      <Overlay open backdrop={<OverlayBackdrop />}>
        <OverlayCenter>
          <FocusTrap
            focusTrapOptions={{
              initialFocus: false,
              onDeactivate: () => onIgnore(),
              clickOutsideDeactivates: false,
              escapeDeactivates: false,
            }}
          >
            <Dialog style={{ maxWidth: toRem(324) }}>
              <Box style={{ padding: config.space.S400 }} direction="Column" gap="700">
                <Text size="T200" align="Center">
                  {getMemberDisplayName(info.room, info.sender) ??
                    getMxIdLocalPart(info.sender) ??
                    info.sender}
                </Text>
                <Box direction="Column" gap="500" alignItems="Center">
                  <Box shrink="No">
                    <Avatar size="500" className={CallAvatarAnimation}>
                      <RoomAvatar
                        roomId={room.roomId}
                        src={avatarUrl}
                        alt={roomName}
                        renderFallback={() => (
                          <RoomIcon
                            roomType={room.getType()}
                            size="400"
                            joinRule={room.getJoinRule()}
                            filled
                          />
                        )}
                      />
                    </Avatar>
                  </Box>
                  <Box grow="Yes" direction="Column" gap="100">
                    <Text size="H3" align="Center" truncate>
                      {roomName}
                    </Text>
                    <Text size="T300">Incoming Call</Text>
                  </Box>
                </Box>
                {!livekitSupported && (
                  <Text
                    style={{ margin: 'auto', color: color.Critical.Main }}
                    size="L400"
                    align="Center"
                  >
                    Your homeserver does not support calling.
                  </Text>
                )}
                {!webRTCSupported() && (
                  <Text
                    style={{ margin: 'auto', color: color.Critical.Main }}
                    size="L400"
                    align="Center"
                  >
                    Your browser does not support WebRTC, which is required for calling.
                  </Text>
                )}
                <Box direction="Column" gap="300">
                  <Button
                    style={{ flexGrow: 1 }}
                    variant="Success"
                    size="400"
                    radii="400"
                    onClick={() => onAnswer(room, info.intent === 'video')}
                    before={
                      <Icon
                        size="200"
                        src={info.intent === 'video' ? Icons.VideoCamera : Icons.Phone}
                        filled
                      />
                    }
                    disabled={!canAnswer}
                  >
                    <Text as="span" size="B400">
                      Answer
                    </Text>
                  </Button>
                  <Button
                    style={{ flexGrow: 1 }}
                    variant="Success"
                    fill="Soft"
                    size="400"
                    radii="400"
                    onClick={() => (dm ? onReject(room, info.refEventId) : onIgnore())}
                    before={<Icon size="200" src={Icons.Cross} filled />}
                  >
                    <Text as="span" size="B400">
                      {dm ? 'Reject' : 'Ignore'}
                    </Text>
                  </Button>
                </Box>
              </Box>
            </Dialog>
          </FocusTrap>
        </OverlayCenter>
      </Overlay>
      <audio ref={audioRef} loop style={{ display: 'none' }}>
        <source src={CallSound} type="audio/ogg" />
      </audio>
    </>
  );
}

type IncomingCallListenerProps = {
  callEmbed?: CallEmbed;
  joined?: boolean;
};
function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps) {
  const mx = useMatrixClient();
  const directs = useAtomValue(mDirectAtom);
  const { navigateRoom } = useRoomNavigate();

  const [callInfo, setCallInfo] = useState<IncomingCallInfo>();
  const dm = callInfo ? directs.has(callInfo.room.roomId) : false;
  const startCall = useCallStart(dm);

  const handleTimelineEvent: EventTimelineSetHandlerMap[RoomEvent.Timeline] = useCallback(
    async (event, room, toStartOfTimeline, removed, data) => {
      // only process rtc notification reference events.
      // we do not want to wait to decrypt all events.
      if (event.getRelation()?.rel_type !== RelationType.Reference) return;

      if (event.isEncrypted()) {
        if (!event.isBeingDecrypted()) {
          await event.attemptDecryption(mx.getCrypto() as CryptoBackend);
        }
        await event.getDecryptionPromise();
      }

      if (
        !room ||
        event.getType() !== EventType.RTCNotification ||
        event.getSender() === mx.getSafeUserId() ||
        !data.liveEvent
      ) {
        return;
      }

      const sender = event.getSender();
      const content = event.getContent<IRTCNotificationContent>();
      const senderTs =
        content.sender_ts - event.getTs() > 20000 ? event.getTs() : content.sender_ts;
      const lifetime = Math.min(content.lifetime, 120000);
      const notificationType = content.notification_type;
      const relation =
        event.getRelation()?.rel_type === RelationType.Reference ? event.getRelation() : undefined;
      const refEventId = relation?.event_id;

      const mention =
        content['m.mentions']?.room ||
        content['m.mentions']?.user_ids?.includes(mx.getSafeUserId());
      if (!sender || !refEventId || !mention || Date.now() >= senderTs + lifetime) {
        return;
      }

      const powerLevelsEvent = getStateEvent(room, StateEvent.RoomPowerLevels);
      const powerLevels = getPowersLevelFromMatrixEvent(powerLevelsEvent);
      const creators = getRoomCreatorsForRoomId(mx, room.roomId);
      const permissions = getRoomPermissionsAPI(creators, powerLevels);

      const hasCallPermission = permissions.stateEvent(
        StateEvent.GroupCallMemberPrefix,
        mx.getSafeUserId(),
      );
      if (!hasCallPermission) return;

      const info: IncomingCallInfo = {
        room,
        sender,
        senderTs,
        lifetime,
        intent:
          'm.call.intent' in content && typeof content['m.call.intent'] === 'string'
            ? content['m.call.intent']
            : undefined,
        notificationType,
        refEventId,
      };

      setCallInfo(info);
    },
    [mx],
  );

  useEffect(() => {
    mx.on(RoomEvent.Timeline, handleTimelineEvent);
    return () => {
      mx.removeListener(RoomEvent.Timeline, handleTimelineEvent);
    };
  }, [mx, handleTimelineEvent]);

  const handleIgnore = useCallback(() => {
    setCallInfo(undefined);
  }, []);

  const handleReject = useCallback(
    (room: Room, eventId: string) => {
      mx.sendEvent(room.roomId, EventType.RTCDecline, {
        'm.relates_to': {
          rel_type: RelationType.Reference,
          event_id: eventId,
        },
      });
      setCallInfo(undefined);
    },
    [mx],
  );

  const handleAnswer = useCallback(
    (room: Room, video: boolean) => {
      startCall(room, { microphone: true, video, sound: true });
      setCallInfo(undefined);
      navigateRoom(room.roomId);
    },
    [startCall, navigateRoom],
  );

  if (callInfo && callEmbed?.roomId === callInfo.room.roomId) {
    return null;
  }
  return !joined && callInfo ? (
    <IncomingCall
      dm={dm}
      info={callInfo}
      onIgnore={handleIgnore}
      onAnswer={handleAnswer}
      onReject={handleReject}
    />
  ) : null;
}

function CallUtils({ embed }: { embed: CallEmbed }) {
  const setCallEmbed = useSetAtom(callEmbedAtom);

  useCallMemberSoundSync(embed);
  useCallThemeSync(embed);
  useCallHangupEvent(
    embed,
    useCallback(() => {
      setCallEmbed(undefined);
    }, [setCallEmbed]),
  );

  return null;
}

type CallEmbedProviderProps = {
  children?: ReactNode;
};
export function CallEmbedProvider({ children }: CallEmbedProviderProps) {
  const callEmbed = useAtomValue(callEmbedAtom);
  const callEmbedRef = useRef<HTMLDivElement>(null);
  const joined = useCallJoined(callEmbed);

  const selectedRoom = useSelectedRoom();
  const chat = useAtomValue(callChatAtom);
  const screenSize = useScreenSizeContext();

  const chatOnlyView = chat && screenSize !== ScreenSize.Desktop;
  const inCallRoom = callEmbed && selectedRoom === callEmbed.roomId;
  const callActive = callEmbed && joined;
  const callVisible = inCallRoom && callActive && !chatOnlyView;
  const pipMode = callActive && !inCallRoom;
  const { navigateRoom } = useRoomNavigate();

  const pipDragRef = React.useRef<{
    startX: number;
    startY: number;
    origLeft: number;
    origTop: number;
    dragged: boolean;
  } | null>(null);
  const activeDragCleanupRef = React.useRef<(() => void) | null>(null);
  React.useEffect(
    () => () => {
      activeDragCleanupRef.current?.();
    },
    [],
  );

  // Track previous pipMode to only reset position when first entering pip (not on callVisible changes)
  const prevPipModeRef = React.useRef(false);
  React.useEffect(() => {
    const el = callEmbedRef.current;
    if (!el) return;
    if (pipMode) {
      if (!prevPipModeRef.current) {
        el.style.top = 'auto';
        el.style.left = 'auto';
        el.style.bottom = '72px';
        el.style.right = '16px';
        el.style.width = '280px';
        el.style.height = '158px';
        el.style.borderRadius = '12px';
        el.style.overflow = 'hidden';
        el.style.zIndex = '99';
        el.style.boxShadow = '0 8px 32px rgba(0,0,0,0.55)';
        el.style.border = '1px solid rgba(255,255,255,0.1)';
      }
      el.style.visibility = 'visible';
    } else {
      [
        'top',
        'left',
        'bottom',
        'right',
        'width',
        'height',
        'borderRadius',
        'overflow',
        'zIndex',
        'boxShadow',
        'border',
      ].forEach((p) => {
        (el.style as any)[p] = '';
      });
      el.style.visibility = callVisible ? '' : 'hidden';
    }
    prevPipModeRef.current = pipMode;
  }, [pipMode, callVisible]);

  React.useEffect(() => {
    if (!pipMode) return;
    const onPipWindowResize = (): void => {
      const el = callEmbedRef.current;
      if (!el) return;
      const l = parseFloat(el.style.left);
      const t = parseFloat(el.style.top);
      if (!isNaN(l))
        el.style.left = `${Math.max(0, Math.min(l, window.innerWidth - el.offsetWidth))}px`;
      if (!isNaN(t))
        el.style.top = `${Math.max(0, Math.min(t, window.innerHeight - el.offsetHeight))}px`;
    };
    window.addEventListener('resize', onPipWindowResize);
    return () => window.removeEventListener('resize', onPipWindowResize);
  }, [pipMode, callEmbedRef]);

  const handlePipMouseDown = (e: React.MouseEvent) => {
    const el = callEmbedRef.current;
    if (!el) return;
    const rect = el.getBoundingClientRect();
    pipDragRef.current = {
      startX: e.clientX,
      startY: e.clientY,
      origLeft: rect.left,
      origTop: rect.top,
      dragged: false,
    };
    const onMove = (ev: MouseEvent) => {
      if (!pipDragRef.current || !el) return;
      const dx = ev.clientX - pipDragRef.current.startX,
        dy = ev.clientY - pipDragRef.current.startY;
      if (!pipDragRef.current.dragged && Math.abs(dx) + Math.abs(dy) > 5) {
        pipDragRef.current.dragged = true;
        document.body.style.cursor = 'grabbing';
        document.body.style.userSelect = 'none';
      }
      if (pipDragRef.current.dragged) {
        el.style.left = `${Math.max(
          0,
          Math.min(window.innerWidth - el.offsetWidth, pipDragRef.current.origLeft + dx),
        )}px`;
        el.style.top = `${Math.max(
          0,
          Math.min(window.innerHeight - el.offsetHeight, pipDragRef.current.origTop + dy),
        )}px`;
        el.style.right = 'auto';
        el.style.bottom = 'auto';
      }
    };
    const onUp = () => {
      document.removeEventListener('mousemove', onMove);
      document.removeEventListener('mouseup', onUp);
      document.body.style.cursor = '';
      document.body.style.userSelect = '';
      activeDragCleanupRef.current = null;
      setTimeout(() => {
        if (pipDragRef.current) pipDragRef.current.dragged = false;
      }, 0);
    };
    activeDragCleanupRef.current = () => {
      document.removeEventListener('mousemove', onMove);
      document.removeEventListener('mouseup', onUp);
      document.body.style.cursor = '';
      document.body.style.userSelect = '';
    };
    document.addEventListener('mousemove', onMove);
    document.addEventListener('mouseup', onUp);
  };

  const handlePipTouchStart = (e: React.TouchEvent) => {
    const el = callEmbedRef.current;
    if (!el || e.touches.length !== 1) return;
    const touch = e.touches[0];
    const rect = el.getBoundingClientRect();
    pipDragRef.current = {
      startX: touch.clientX,
      startY: touch.clientY,
      origLeft: rect.left,
      origTop: rect.top,
      dragged: false,
    };
    const onTouchMove = (ev: TouchEvent) => {
      if (!pipDragRef.current || !el || ev.touches.length !== 1) return;
      ev.preventDefault();
      const t = ev.touches[0];
      const dx = t.clientX - pipDragRef.current.startX;
      const dy = t.clientY - pipDragRef.current.startY;
      if (!pipDragRef.current.dragged && Math.abs(dx) + Math.abs(dy) > 5)
        pipDragRef.current.dragged = true;
      if (pipDragRef.current.dragged) {
        el.style.left = `${Math.max(
          0,
          Math.min(window.innerWidth - el.offsetWidth, pipDragRef.current.origLeft + dx),
        )}px`;
        el.style.top = `${Math.max(
          0,
          Math.min(window.innerHeight - el.offsetHeight, pipDragRef.current.origTop + dy),
        )}px`;
        el.style.right = 'auto';
        el.style.bottom = 'auto';
      }
    };
    const onTouchEnd = () => {
      document.removeEventListener('touchmove', onTouchMove);
      document.removeEventListener('touchend', onTouchEnd);
      activeDragCleanupRef.current = null;
      setTimeout(() => {
        if (pipDragRef.current) pipDragRef.current.dragged = false;
      }, 0);
    };
    activeDragCleanupRef.current = () => {
      document.removeEventListener('touchmove', onTouchMove);
      document.removeEventListener('touchend', onTouchEnd);
    };
    document.addEventListener('touchmove', onTouchMove, { passive: false });
    document.addEventListener('touchend', onTouchEnd);
  };

  const handleResizeMouseDown = (e: React.MouseEvent, corner: Corner) => {
    e.stopPropagation();
    e.preventDefault();
    const el = callEmbedRef.current;
    if (!el) return;
    normaliseToTopLeft(el);
    const sx = e.clientX,
      sy = e.clientY,
      sw = el.offsetWidth,
      sh = el.offsetHeight;
    const sl = parseFloat(el.style.left),
      st = parseFloat(el.style.top);
    document.body.style.cursor = `${corner}-resize`;
    document.body.style.userSelect = 'none';
    const onMove = (ev: MouseEvent) => {
      const dx = ev.clientX - sx,
        dy = ev.clientY - sy;
      let w = sw,
        h = sh,
        l = sl,
        t = st;
      if (corner === 'se') {
        w = sw + dx;
        h = sh + dy;
      }
      if (corner === 'sw') {
        w = sw - dx;
        h = sh + dy;
        l = sl + sw - Math.max(PIP_MIN_W, w);
      }
      if (corner === 'ne') {
        w = sw + dx;
        h = sh - dy;
        t = st + sh - Math.max(PIP_MIN_H, h);
      }
      if (corner === 'nw') {
        w = sw - dx;
        h = sh - dy;
        l = sl + sw - Math.max(PIP_MIN_W, w);
        t = st + sh - Math.max(PIP_MIN_H, h);
      }
      w = Math.max(PIP_MIN_W, Math.min(w, window.innerWidth));
      h = Math.max(PIP_MIN_H, Math.min(h, window.innerHeight));
      l = Math.max(0, Math.min(l, window.innerWidth - PIP_MIN_W));
      t = Math.max(0, Math.min(t, window.innerHeight - PIP_MIN_H));
      el.style.width = `${w}px`;
      el.style.height = `${h}px`;
      el.style.left = `${l}px`;
      el.style.top = `${t}px`;
    };
    const onUp = () => {
      document.removeEventListener('mousemove', onMove);
      document.removeEventListener('mouseup', onUp);
      document.body.style.cursor = '';
      document.body.style.userSelect = '';
      activeDragCleanupRef.current = null;
    };
    activeDragCleanupRef.current = () => {
      document.removeEventListener('mousemove', onMove);
      document.removeEventListener('mouseup', onUp);
      document.body.style.cursor = '';
      document.body.style.userSelect = '';
    };
    document.addEventListener('mousemove', onMove);
    document.addEventListener('mouseup', onUp);
  };

  return (
    <CallEmbedContextProvider value={callEmbed}>
      {callEmbed && <CallUtils embed={callEmbed} />}
      <CallEmbedRefContextProvider value={callEmbedRef}>
        <IncomingCallListener callEmbed={callEmbed} joined={joined} />
        {children}
      </CallEmbedRefContextProvider>
      <div
        data-call-embed-container
        style={{
          visibility: callVisible ? undefined : 'hidden',
          position: 'fixed',
          top: 0,
          left: 0,
          width: '100%',
          height: '50%',
        }}
        ref={callEmbedRef}
      >
        {pipMode && callEmbed && (
          <>
            <div
              role="button"
              tabIndex={0}
              aria-label="Return to call"
              onMouseDown={handlePipMouseDown}
              onTouchStart={handlePipTouchStart}
              onClick={() => {
                if (!pipDragRef.current?.dragged) navigateRoom(callEmbed.roomId);
              }}
              onKeyDown={(e) => e.key === 'Enter' && navigateRoom(callEmbed.roomId)}
              style={{
                position: 'absolute',
                inset: 0,
                zIndex: 1,
                background: 'transparent',
                cursor: 'grab',
                display: 'flex',
                alignItems: 'flex-start',
                justifyContent: 'flex-end',
                padding: '6px',
              }}
            >
              <div
                style={{
                  background: 'rgba(0,0,0,0.65)',
                  backdropFilter: 'blur(4px)',
                  borderRadius: '6px',
                  padding: '3px 8px',
                  color: '#fff',
                  fontSize: '11px',
                  fontWeight: 600,
                  pointerEvents: 'none',
                }}
              >
                ↗ Return to call
              </div>
            </div>
            {(['se', 'sw', 'ne', 'nw'] as Corner[]).map((corner) => {
              const s = corner.includes('s');
              const e2 = corner.includes('e');
              const dots = [
                [2, 2],
                [2, 7],
                [7, 2],
              ].map(([a, b]) => ({
                position: 'absolute' as const,
                width: 4,
                height: 4,
                borderRadius: '50%',
                background: 'rgba(255,255,255,0.45)',
                [s ? 'bottom' : 'top']: a,
                [e2 ? 'right' : 'left']: b,
              }));
              return (
                <div
                  key={corner}
                  onMouseDown={(ev) => handleResizeMouseDown(ev, corner)}
                  onClick={(ev) => ev.stopPropagation()}
                  style={{
                    position: 'absolute',
                    width: '18px',
                    height: '18px',
                    [s ? 'bottom' : 'top']: 0,
                    [e2 ? 'right' : 'left']: 0,
                    cursor: `${corner}-resize`,
                    zIndex: 2,
                  }}
                >
                  {dots.map((style, i) => (
                    <div key={i} style={style} />
                  ))}
                </div>
              );
            })}
          </>
        )}
      </div>
    </CallEmbedContextProvider>
  );
}
