// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only

import {
  useEffect,
  useMemo,
  useCallback,
  type JSX,
  type RefObject,
} from 'react';
import lodash from 'lodash';
import type { VideoFrameSource } from '@signalapp/ringrtc';
import { Avatar, AvatarSize } from './Avatar.dom.tsx';
import { CallBackgroundBlur } from './CallBackgroundBlur.dom.tsx';
import { DirectCallRemoteParticipant } from './DirectCallRemoteParticipant.dom.tsx';
import { GroupCallRemoteParticipant } from './GroupCallRemoteParticipant.dom.tsx';
import type { LocalizerType } from '../types/Util.std.ts';
import {
  GroupCallJoinState,
  type ActiveCallType,
  type GroupCallRemoteParticipantType,
  type GroupCallVideoRequest,
} from '../types/Calling.std.ts';
import { CallMode } from '../types/CallDisposition.std.ts';
import { AvatarColors } from '../types/Colors.std.ts';
import type { SetRendererCanvasType } from '../state/ducks/calling.preload.ts';
import { useGetCallingFrameBuffer } from '../calling/useGetCallingFrameBuffer.std.ts';
import { MAX_FRAME_HEIGHT } from '../calling/constants.std.ts';
import { usePageVisibility } from '../hooks/usePageVisibility.dom.ts';
import { missingCaseError } from '../util/missingCaseError.std.ts';
import { nonRenderedRemoteParticipant } from '../util/ringrtc/nonRenderedRemoteParticipant.std.ts';
import { isReconnecting } from '../util/callingIsReconnecting.std.ts';
import { isGroupOrAdhocActiveCall } from '../util/isGroupOrAdhocCall.std.ts';
import { assertDev } from '../util/assert.std.ts';
import type { CallingImageDataCache } from './CallManager.dom.tsx';
import {
  PIP_MAXIMUM_HEIGHT_MULTIPLIER,
  PIP_MINIMUM_HEIGHT_MULTIPLIER,
  PIP_WIDTH_NORMAL,
} from './CallingPip.dom.tsx';

const { clamp, isNumber, maxBy } = lodash;

function BlurredBackground({
  activeCall,
  activeGroupCallSpeaker,
  avatarSize,
  darken,
  i18n,
}: {
  activeCall: ActiveCallType;
  activeGroupCallSpeaker?: undefined | GroupCallRemoteParticipantType;
  avatarSize: AvatarSize;
  darken?: boolean;
  i18n: LocalizerType;
}): JSX.Element {
  const {
    avatarPlaceholderGradient,
    color,
    type: conversationType,
    phoneNumber,
    profileName,
    title,
  } = activeCall.conversation;
  const avatarUrl =
    activeGroupCallSpeaker?.avatarUrl ?? activeCall.conversation.avatarUrl;

  return (
    <CallBackgroundBlur avatarUrl={avatarUrl} darken={darken}>
      <div className="module-calling-pip__video--avatar">
        <Avatar
          avatarPlaceholderGradient={avatarPlaceholderGradient}
          avatarUrl={avatarUrl}
          badge={undefined}
          color={color || AvatarColors[0]}
          noteToSelf={false}
          conversationType={conversationType}
          i18n={i18n}
          phoneNumber={phoneNumber}
          profileName={profileName}
          title={title}
          size={avatarSize}
        />
      </div>
    </CallBackgroundBlur>
  );
}

export type PropsType = {
  activeCall: ActiveCallType;
  getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
  i18n: LocalizerType;
  imageDataCache: RefObject<CallingImageDataCache>;
  setGroupCallVideoRequest: (
    _: Array<GroupCallVideoRequest>,
    speakerHeight: number
  ) => void;
  setRendererCanvas: (_: SetRendererCanvasType) => void;
  height: number;
  width: number;
  updateHeight: (newHeight: number) => void;
};

export function CallingPipRemoteVideo({
  activeCall,
  getGroupCallVideoFrameSource,
  imageDataCache,
  i18n,
  setGroupCallVideoRequest,
  setRendererCanvas,
  height,
  width,
  updateHeight,
}: PropsType): JSX.Element {
  const { conversation } = activeCall;

  const getGroupCallFrameBuffer = useGetCallingFrameBuffer();

  const isPageVisible = usePageVisibility();

  const activeGroupCallSpeaker: undefined | GroupCallRemoteParticipantType =
    useMemo(() => {
      if (!isGroupOrAdhocActiveCall(activeCall)) {
        return undefined;
      }

      if (activeCall.joinState !== GroupCallJoinState.Joined) {
        return undefined;
      }

      return maxBy(activeCall.remoteParticipants, participant =>
        participant.presenting ? Infinity : participant.speakerTime || -Infinity
      );
    }, [activeCall]);

  useEffect(() => {
    if (!isGroupOrAdhocActiveCall(activeCall)) {
      return;
    }

    if (!activeGroupCallSpeaker || !activeGroupCallSpeaker.hasRemoteVideo) {
      return;
    }
    const { videoAspectRatio } = activeGroupCallSpeaker;
    if (!isNumber(videoAspectRatio)) {
      return;
    }

    const portraitRatio = 1 / videoAspectRatio;
    const newHeight = clamp(
      Math.floor(width * portraitRatio),
      1,
      MAX_FRAME_HEIGHT
    );

    // Update only for portrait video that fits, otherwise leave things as they are
    if (
      newHeight !== height &&
      portraitRatio >= PIP_MINIMUM_HEIGHT_MULTIPLIER &&
      portraitRatio <= PIP_MAXIMUM_HEIGHT_MULTIPLIER
    ) {
      updateHeight(newHeight);
    }

    if (isPageVisible) {
      const participants = activeCall.remoteParticipants.map(participant => {
        if (participant === activeGroupCallSpeaker) {
          return {
            demuxId: participant.demuxId,
            width,
            height: newHeight,
          };
        }
        return nonRenderedRemoteParticipant(participant);
      });
      setGroupCallVideoRequest(
        participants,
        // When there's a presenter, we do not want the SFU to prioritize the speaker feed
        activeGroupCallSpeaker.presenting ? 0 : newHeight
      );
    } else {
      setGroupCallVideoRequest(
        activeCall.remoteParticipants.map(nonRenderedRemoteParticipant),
        0
      );
    }
  }, [
    activeCall,
    activeGroupCallSpeaker,
    height,
    isPageVisible,
    setGroupCallVideoRequest,
    updateHeight,
    width,
  ]);

  const handleDirectSize = useCallback(
    (newSize: { width: number; height: number }) => {
      const portraitRatio = newSize.height / newSize.width;
      const newHeight = clamp(
        Math.floor(width * portraitRatio),
        1,
        MAX_FRAME_HEIGHT
      );

      // Update only for portrait video that fits, otherwise leave things as they are
      if (
        newHeight !== height &&
        portraitRatio >= PIP_MINIMUM_HEIGHT_MULTIPLIER &&
        portraitRatio <= PIP_MAXIMUM_HEIGHT_MULTIPLIER
      ) {
        updateHeight(newHeight);
      }
    },
    [height, updateHeight, width]
  );

  const avatarSize =
    width > PIP_WIDTH_NORMAL ? AvatarSize.NINETY_SIX : AvatarSize.SIXTY_FOUR;

  switch (activeCall.callMode) {
    case CallMode.Direct: {
      const { hasRemoteVideo } = activeCall.remoteParticipants[0];
      if (!hasRemoteVideo) {
        return (
          <div className="module-calling-pip__video--remote">
            <BlurredBackground
              activeCall={activeCall}
              avatarSize={avatarSize}
              i18n={i18n}
            />
          </div>
        );
      }
      assertDev(
        conversation.type === 'direct',
        'CallingPipRemoteVideo for direct call must be associated with direct conversation'
      );
      return (
        <div className="module-calling-pip__video--remote">
          <BlurredBackground
            activeCall={activeCall}
            avatarSize={avatarSize}
            darken
            i18n={i18n}
          />
          <DirectCallRemoteParticipant
            conversation={conversation}
            hasRemoteVideo={hasRemoteVideo}
            handleSize={handleDirectSize}
            i18n={i18n}
            isReconnecting={isReconnecting(activeCall)}
            setRendererCanvas={setRendererCanvas}
          />
        </div>
      );
    }
    case CallMode.Group:
    case CallMode.Adhoc:
      if (!activeGroupCallSpeaker) {
        return (
          <div className="module-calling-pip__video--remote">
            <BlurredBackground
              activeCall={activeCall}
              avatarSize={avatarSize}
              i18n={i18n}
            />
          </div>
        );
      }
      return (
        <div className="module-calling-pip__video--remote">
          <BlurredBackground
            activeCall={activeCall}
            activeGroupCallSpeaker={activeGroupCallSpeaker}
            avatarSize={avatarSize}
            darken={activeGroupCallSpeaker.hasRemoteVideo}
            i18n={i18n}
          />
          <GroupCallRemoteParticipant
            getFrameBuffer={getGroupCallFrameBuffer}
            getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
            imageDataCache={imageDataCache}
            i18n={i18n}
            isInPip
            joinedAt={activeCall.joinedAt}
            remoteParticipant={activeGroupCallSpeaker}
            remoteParticipantsCount={activeCall.remoteParticipants.length}
            isActiveSpeakerInSpeakerView={false}
            isCallReconnecting={isReconnecting(activeCall)}
          />
        </div>
      );
    default:
      throw missingCaseError(activeCall);
  }
}
