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

import type { ReactElement } from 'react';
import React, { useRef, useState, useEffect } from 'react';
import classNames from 'classnames';
import type { VideoFrameSource } from '@signalapp/ringrtc';
import type { LocalizerType } from '../types/Util';
import type { GroupCallRemoteParticipantType } from '../types/Calling';
import { GroupCallRemoteParticipant } from './GroupCallRemoteParticipant';

const OVERFLOW_SCROLLED_TO_EDGE_THRESHOLD = 20;
const OVERFLOW_SCROLL_BUTTON_RATIO = 0.75;

// This should be an integer, as sub-pixel widths can cause performance issues.
export const OVERFLOW_PARTICIPANT_WIDTH = 140;

export type PropsType = {
  getFrameBuffer: () => Buffer;
  getGroupCallVideoFrameSource: (demuxId: number) => VideoFrameSource;
  i18n: LocalizerType;
  isCallReconnecting: boolean;
  onParticipantVisibilityChanged: (
    demuxId: number,
    isVisible: boolean
  ) => unknown;
  overflowedParticipants: ReadonlyArray<GroupCallRemoteParticipantType>;
  remoteAudioLevels: Map<number, number>;
  remoteParticipantsCount: number;
};

export function GroupCallOverflowArea({
  getFrameBuffer,
  getGroupCallVideoFrameSource,
  i18n,
  isCallReconnecting,
  onParticipantVisibilityChanged,
  overflowedParticipants,
  remoteAudioLevels,
  remoteParticipantsCount,
}: PropsType): JSX.Element | null {
  const overflowRef = useRef<HTMLDivElement | null>(null);
  const [overflowScrollTop, setOverflowScrollTop] = useState(0);

  // This assumes that these values will change along with re-renders. If that's not true,
  //   we should add these values to the component's state.
  let visibleHeight: number;
  let scrollMax: number;
  if (overflowRef.current) {
    visibleHeight = overflowRef.current.clientHeight;
    scrollMax = overflowRef.current.scrollHeight - visibleHeight;
  } else {
    visibleHeight = 0;
    scrollMax = 0;
  }

  const hasOverflowedParticipants = Boolean(overflowedParticipants.length);

  useEffect(() => {
    // If there aren't any overflowed participants, we want to clear the scroll position
    //   so we don't hold onto stale values.
    if (!hasOverflowedParticipants) {
      setOverflowScrollTop(0);
    }
  }, [hasOverflowedParticipants]);

  if (!hasOverflowedParticipants) {
    return null;
  }

  const isScrolledToTop =
    overflowScrollTop < OVERFLOW_SCROLLED_TO_EDGE_THRESHOLD;
  const isScrolledToBottom =
    overflowScrollTop > scrollMax - OVERFLOW_SCROLLED_TO_EDGE_THRESHOLD;

  return (
    <div
      className="module-ongoing-call__participants__overflow"
      style={{
        // This width could live in CSS but we put it here to avoid having to keep two
        //   values in sync.
        width: OVERFLOW_PARTICIPANT_WIDTH,
      }}
    >
      <OverflowAreaScrollMarker
        i18n={i18n}
        isHidden={isScrolledToTop}
        onClick={() => {
          const el = overflowRef.current;
          if (!el) {
            return;
          }
          el.scrollTo({
            top: Math.max(
              el.scrollTop - visibleHeight * OVERFLOW_SCROLL_BUTTON_RATIO,
              0
            ),
            left: 0,
            behavior: 'smooth',
          });
        }}
        placement="top"
      />
      <div
        className="module-ongoing-call__participants__overflow__inner"
        ref={overflowRef}
        onScroll={() => {
          // Ideally this would use `event.target.scrollTop`, but that does not seem to be
          //   available, so we use the ref.
          const el = overflowRef.current;
          if (!el) {
            return;
          }
          setOverflowScrollTop(el.scrollTop);
        }}
      >
        {overflowedParticipants.map(remoteParticipant => (
          <GroupCallRemoteParticipant
            key={remoteParticipant.demuxId}
            getFrameBuffer={getFrameBuffer}
            getGroupCallVideoFrameSource={getGroupCallVideoFrameSource}
            i18n={i18n}
            audioLevel={remoteAudioLevels.get(remoteParticipant.demuxId) ?? 0}
            onVisibilityChanged={onParticipantVisibilityChanged}
            width={OVERFLOW_PARTICIPANT_WIDTH}
            height={Math.floor(
              OVERFLOW_PARTICIPANT_WIDTH / remoteParticipant.videoAspectRatio
            )}
            remoteParticipant={remoteParticipant}
            remoteParticipantsCount={remoteParticipantsCount}
            isActiveSpeakerInSpeakerView={false}
            isCallReconnecting={isCallReconnecting}
          />
        ))}
      </div>
      <OverflowAreaScrollMarker
        i18n={i18n}
        isHidden={isScrolledToBottom}
        onClick={() => {
          const el = overflowRef.current;
          if (!el) {
            return;
          }
          el.scrollTo({
            top: Math.min(
              el.scrollTop + visibleHeight * OVERFLOW_SCROLL_BUTTON_RATIO,
              scrollMax
            ),
            left: 0,
            behavior: 'smooth',
          });
        }}
        placement="bottom"
      />
    </div>
  );
}

function OverflowAreaScrollMarker({
  i18n,
  isHidden,
  onClick,
  placement,
}: {
  i18n: LocalizerType;
  isHidden: boolean;
  onClick: () => void;
  placement: 'top' | 'bottom';
}): ReactElement {
  const baseClassName =
    'module-ongoing-call__participants__overflow__scroll-marker';

  return (
    <div
      className={classNames(baseClassName, `${baseClassName}--${placement}`, {
        [`${baseClassName}--hidden`]: isHidden,
      })}
    >
      <button
        type="button"
        className={`${baseClassName}__button`}
        onClick={onClick}
        aria-label={
          placement === 'top'
            ? i18n('icu:calling__overflow__scroll-up')
            : i18n('icu:calling__overflow__scroll-down')
        }
      />
    </div>
  );
}
