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

import React, { useEffect, useMemo, useRef } from 'react';
import type { ActiveCallType } from '../types/Calling';
import { CallMode } from '../types/Calling';
import type { ConversationType } from '../state/ducks/conversations';
import type { LocalizerType } from '../types/Util';
import { CallingToastProvider, useCallingToasts } from './CallingToast';
import { usePrevious } from '../hooks/usePrevious';
import { difference as setDifference } from '../util/setUtil';
import { isMoreRecentThan } from '../util/timestamp';

type PropsType = {
  activeCall: ActiveCallType;
  i18n: LocalizerType;
};

const ME = Symbol('me');

function getCurrentPresenter(
  activeCall: Readonly<ActiveCallType>
): ConversationType | { id: typeof ME } | undefined {
  if (activeCall.presentingSource) {
    return { id: ME };
  }
  if (activeCall.callMode === CallMode.Direct) {
    const isOtherPersonPresenting = activeCall.remoteParticipants.some(
      participant => participant.presenting
    );
    return isOtherPersonPresenting ? activeCall.conversation : undefined;
  }
  if (activeCall.callMode === CallMode.Group) {
    return activeCall.remoteParticipants.find(
      participant => participant.presenting
    );
  }
  return undefined;
}

export function useScreenSharingStoppedToast({
  activeCall,
  i18n,
}: PropsType): void {
  const { showToast, hideToast } = useCallingToasts();

  const SOMEONE_STOPPED_PRESENTING_TOAST_KEY = 'someone_stopped_presenting';

  const currentPresenter = useMemo(
    () => getCurrentPresenter(activeCall),
    [activeCall]
  );
  const previousPresenter = usePrevious(currentPresenter, currentPresenter);

  useEffect(() => {
    if (previousPresenter && !currentPresenter) {
      hideToast(SOMEONE_STOPPED_PRESENTING_TOAST_KEY);
      showToast({
        key: SOMEONE_STOPPED_PRESENTING_TOAST_KEY,
        content:
          previousPresenter.id === ME
            ? i18n('icu:calling__presenting--you-stopped')
            : i18n('icu:calling__presenting--person-stopped', {
                name: previousPresenter.title,
              }),
        autoClose: true,
      });
    }
  }, [
    activeCall,
    hideToast,
    currentPresenter,
    previousPresenter,
    showToast,
    i18n,
  ]);
}

function useMutedToast({
  hasLocalAudio,
  i18n,
}: {
  hasLocalAudio: boolean;
  i18n: LocalizerType;
}): void {
  const previousHasLocalAudio = usePrevious(hasLocalAudio, hasLocalAudio);
  const { showToast, hideToast } = useCallingToasts();
  const MUTED_TOAST_KEY = 'muted';

  useEffect(() => {
    if (
      previousHasLocalAudio !== undefined &&
      hasLocalAudio !== previousHasLocalAudio
    ) {
      hideToast(MUTED_TOAST_KEY);
      showToast({
        key: MUTED_TOAST_KEY,
        content: hasLocalAudio
          ? i18n('icu:CallControls__MutedToast--unmuted')
          : i18n('icu:CallControls__MutedToast--muted'),
        autoClose: true,
        dismissable: true,
      });
    }
  }, [hasLocalAudio, previousHasLocalAudio, hideToast, showToast, i18n]);
}

function useOutgoingRingToast({
  outgoingRing,
  i18n,
}: {
  outgoingRing?: boolean;
  i18n: LocalizerType;
}): void {
  const { showToast, hideToast } = useCallingToasts();
  const previousOutgoingRing = usePrevious(outgoingRing, outgoingRing);
  const RINGING_TOAST_KEY = 'ringing';

  React.useEffect(() => {
    if (outgoingRing === undefined) {
      return;
    }

    if (
      previousOutgoingRing !== undefined &&
      outgoingRing !== previousOutgoingRing
    ) {
      hideToast(RINGING_TOAST_KEY);
      showToast({
        key: RINGING_TOAST_KEY,
        content: outgoingRing
          ? i18n('icu:CallControls__RingingToast--ringing-on')
          : i18n('icu:CallControls__RingingToast--ringing-off'),
        autoClose: true,
        dismissable: true,
      });
    }
  }, [outgoingRing, previousOutgoingRing, hideToast, showToast, i18n]);
}

function useRaisedHandsToast({
  raisedHands,
  renderRaisedHandsToast,
}: {
  raisedHands?: Set<number>;
  renderRaisedHandsToast?: (
    hands: Array<number>
  ) => JSX.Element | string | undefined;
}): void {
  const RAISED_HANDS_TOAST_KEY = 'raised-hands';
  const RAISED_HANDS_TOAST_LIFETIME = 4000;
  const LOAD_DELAY = 2000;

  const { showToast, hideToast } = useCallingToasts();

  // Hand state is updated after a delay upon joining a call, so it can appear that
  // hands were raised immediately when you join a call. To avoid spurious toasts, add
  // an initial delay before showing toasts.
  const [isLoaded, setIsLoaded] = React.useState<boolean>(false);
  React.useEffect(() => {
    const timeout = setTimeout(() => {
      setIsLoaded(true);
    }, LOAD_DELAY);
    return () => clearTimeout(timeout);
  }, []);

  const previousRaisedHands = usePrevious(raisedHands, raisedHands);
  const [newHands, loweredHands]: [Set<number>, Set<number>] = isLoaded
    ? [
        setDifference(
          raisedHands ?? new Set(),
          previousRaisedHands ?? new Set()
        ),
        setDifference(
          previousRaisedHands ?? new Set(),
          raisedHands ?? new Set()
        ),
      ]
    : [new Set(), new Set()];

  const toastLastShownAt = useRef<number>(0);
  const handsForLastShownToast = useRef<Set<number>>(new Set());

  React.useEffect(() => {
    if (raisedHands?.size === 0) {
      hideToast(RAISED_HANDS_TOAST_KEY);
    }

    if (
      (newHands.size === 0 && loweredHands.size === 0) ||
      !renderRaisedHandsToast
    ) {
      return;
    }

    // If there's an existing raised hand toast (it hasn't faded out yet), then
    // group the newly raised and lowered hands into the existing toast.
    let handsForToast: Array<number>;
    if (
      isMoreRecentThan(toastLastShownAt.current, RAISED_HANDS_TOAST_LIFETIME)
    ) {
      handsForToast = [
        ...setDifference(handsForLastShownToast.current, loweredHands),
        ...newHands,
      ];

      // If someone lowered a hand which isn't present in the existing toast,
      // we can ignore it.
      if (
        newHands.size === 0 &&
        loweredHands.size > 0 &&
        handsForToast.length &&
        handsForToast.length === handsForLastShownToast.current.size
      ) {
        return;
      }
    } else {
      handsForToast = [...newHands];
    }
    handsForLastShownToast.current = new Set([...handsForToast]);

    hideToast(RAISED_HANDS_TOAST_KEY);

    const content = renderRaisedHandsToast(handsForToast.reverse());
    if (!content) {
      return;
    }

    // Note: Don't set { dismissable: true } or else the links (Lower or View Queue)
    // will cause nested buttons (dismissable toasts are <button>s)
    showToast({
      key: RAISED_HANDS_TOAST_KEY,
      content,
      autoClose: true,
      lifetime: RAISED_HANDS_TOAST_LIFETIME,
    });
    toastLastShownAt.current = Date.now();
  }, [
    raisedHands,
    previousRaisedHands,
    newHands,
    handsForLastShownToast,
    loweredHands,
    renderRaisedHandsToast,
    hideToast,
    showToast,
  ]);
}

type CallingButtonToastsType = {
  hasLocalAudio: boolean;
  outgoingRing: boolean | undefined;
  raisedHands?: Set<number>;
  renderRaisedHandsToast?: (
    hands: Array<number>
  ) => JSX.Element | string | undefined;
  i18n: LocalizerType;
};

export function CallingButtonToastsContainer(
  props: CallingButtonToastsType
): JSX.Element {
  const toastRegionRef = useRef<HTMLDivElement>(null);
  return (
    <CallingToastProvider
      i18n={props.i18n}
      maxNonPersistentToasts={1}
      region={toastRegionRef}
    >
      <div className="CallingButtonToasts__outer">
        <div className="CallingButtonToasts" ref={toastRegionRef} />
        <CallingButtonToasts {...props} />
      </div>
    </CallingToastProvider>
  );
}

function CallingButtonToasts({
  hasLocalAudio,
  outgoingRing,
  raisedHands,
  renderRaisedHandsToast,
  i18n,
}: CallingButtonToastsType) {
  useMutedToast({ hasLocalAudio, i18n });
  useOutgoingRingToast({ outgoingRing, i18n });
  useRaisedHandsToast({ raisedHands, renderRaisedHandsToast });

  return null;
}
