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

import React, {
  MouseEvent,
  ReactNode,
  useCallback,
  useEffect,
  useRef,
  useState,
} from 'react';
import classNames from 'classnames';
import moment from 'moment';
import { createPortal } from 'react-dom';
import { noop } from 'lodash';

import * as GoogleChrome from '../util/GoogleChrome';
import { AttachmentType, isGIF } from '../types/Attachment';
import { Avatar, AvatarSize } from './Avatar';
import { ConversationType } from '../state/ducks/conversations';
import { IMAGE_PNG, isImage, isVideo } from '../types/MIME';
import { LocalizerType } from '../types/Util';
import { MediaItemType, MessageAttributesType } from '../types/MediaItem';
import { formatDuration } from '../util/formatDuration';

export type PropsType = {
  children?: ReactNode;
  close: () => void;
  getConversation?: (id: string) => ConversationType;
  i18n: LocalizerType;
  isViewOnce?: boolean;
  media: Array<MediaItemType>;
  onForward?: (messageId: string) => void;
  onSave?: (options: {
    attachment: AttachmentType;
    message: MessageAttributesType;
    index: number;
  }) => void;
  selectedIndex?: number;
};

export function Lightbox({
  children,
  close,
  getConversation,
  media,
  i18n,
  isViewOnce = false,
  onForward,
  onSave,
  selectedIndex: initialSelectedIndex = 0,
}: PropsType): JSX.Element | null {
  const [root, setRoot] = React.useState<HTMLElement | undefined>();
  const [selectedIndex, setSelectedIndex] = useState<number>(
    initialSelectedIndex
  );

  const [previousFocus, setPreviousFocus] = useState<HTMLElement | undefined>();
  const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>(
    null
  );
  const [videoTime, setVideoTime] = useState<number | undefined>();
  const [zoomed, setZoomed] = useState(false);
  const containerRef = useRef<HTMLDivElement | null>(null);
  const focusRef = useRef<HTMLDivElement | null>(null);

  const restorePreviousFocus = useCallback(() => {
    if (previousFocus && previousFocus.focus) {
      previousFocus.focus();
    }
  }, [previousFocus]);

  const onPrevious = useCallback(() => {
    setSelectedIndex(prevSelectedIndex => Math.max(prevSelectedIndex - 1, 0));
  }, []);

  const onNext = useCallback(() => {
    setSelectedIndex(prevSelectedIndex =>
      Math.min(prevSelectedIndex + 1, media.length - 1)
    );
  }, [media]);

  const onTimeUpdate = useCallback(() => {
    if (!videoElement) {
      return;
    }
    setVideoTime(videoElement.currentTime);
  }, [setVideoTime, videoElement]);

  const handleSave = () => {
    const mediaItem = media[selectedIndex];
    const { attachment, message, index } = mediaItem;

    onSave?.({ attachment, message, index });
  };

  const handleForward = () => {
    close();
    const mediaItem = media[selectedIndex];
    onForward?.(mediaItem.message.id);
  };

  const onKeyDown = useCallback(
    (event: KeyboardEvent) => {
      switch (event.key) {
        case 'Escape':
          if (zoomed) {
            setZoomed(false);
          } else {
            close();
          }

          event.preventDefault();
          event.stopPropagation();

          break;

        case 'ArrowLeft':
          if (onPrevious) {
            onPrevious();

            event.preventDefault();
            event.stopPropagation();
          }
          break;

        case 'ArrowRight':
          if (onNext) {
            onNext();

            event.preventDefault();
            event.stopPropagation();
          }
          break;

        default:
      }
    },
    [close, onNext, onPrevious, zoomed]
  );

  const stopPropagationAndClose = (event: MouseEvent<HTMLElement>) => {
    event.stopPropagation();
    close();
  };

  const playVideo = useCallback(() => {
    if (!videoElement) {
      return;
    }

    if (videoElement.paused) {
      videoElement.play();
    } else {
      videoElement.pause();
    }
  }, [videoElement]);

  useEffect(() => {
    const div = document.createElement('div');
    document.body.appendChild(div);
    setRoot(div);

    return () => {
      document.body.removeChild(div);
      setRoot(undefined);
    };
  }, []);

  useEffect(() => {
    if (!previousFocus) {
      setPreviousFocus(document.activeElement as HTMLElement);
    }
  }, [previousFocus]);

  useEffect(() => {
    return () => {
      restorePreviousFocus();
    };
  }, [restorePreviousFocus]);

  useEffect(() => {
    const useCapture = true;
    document.addEventListener('keydown', onKeyDown, useCapture);

    return () => {
      document.removeEventListener('keydown', onKeyDown, useCapture);
    };
  }, [onKeyDown]);

  useEffect(() => {
    playVideo();

    if (focusRef && focusRef.current) {
      focusRef.current.focus();
    }

    if (videoElement && isViewOnce) {
      videoElement.addEventListener('timeupdate', onTimeUpdate);

      return () => {
        videoElement.removeEventListener('timeupdate', onTimeUpdate);
      };
    }

    return noop;
  }, [isViewOnce, onTimeUpdate, playVideo, videoElement]);

  const { attachment, contentType, loop = false, objectURL, message } =
    media[selectedIndex] || {};
  const caption = attachment?.caption;

  let content: JSX.Element;
  if (!contentType) {
    content = <>{children}</>;
  } else {
    const isImageTypeSupported = GoogleChrome.isImageTypeSupported(contentType);
    const isVideoTypeSupported = GoogleChrome.isVideoTypeSupported(contentType);
    const isUnsupportedImageType =
      !isImageTypeSupported && isImage(contentType);
    const isUnsupportedVideoType =
      !isVideoTypeSupported && isVideo(contentType);

    if (isImageTypeSupported) {
      if (objectURL) {
        content = (
          <button
            className="Lightbox__zoom-button"
            onClick={() => setZoomed(!zoomed)}
            type="button"
          >
            <img
              alt={i18n('lightboxImageAlt')}
              className="Lightbox__object"
              onContextMenu={(event: MouseEvent<HTMLImageElement>) => {
                // These are the only image types supported by Electron's NativeImage
                if (
                  event &&
                  contentType !== IMAGE_PNG &&
                  !/image\/jpe?g/g.test(contentType)
                ) {
                  event.preventDefault();
                }
              }}
              src={objectURL}
            />
          </button>
        );
      } else {
        content = (
          <button
            aria-label={i18n('lightboxImageAlt')}
            className={classNames({
              Lightbox__object: true,
              Lightbox__unsupported: true,
              'Lightbox__unsupported--missing': true,
            })}
            onClick={stopPropagationAndClose}
            type="button"
          />
        );
      }
    } else if (isVideoTypeSupported) {
      const shouldLoop = loop || isGIF([attachment]) || isViewOnce;
      content = (
        <video
          className="Lightbox__object"
          controls={!shouldLoop}
          key={objectURL}
          loop={shouldLoop}
          ref={setVideoElement}
        >
          <source src={objectURL} />
        </video>
      );
    } else if (isUnsupportedImageType || isUnsupportedVideoType) {
      content = (
        <button
          aria-label={i18n('unsupportedAttachment')}
          className={classNames({
            Lightbox__object: true,
            Lightbox__unsupported: true,
            'Lightbox__unsupported--image': isUnsupportedImageType,
            'Lightbox__unsupported--video': isUnsupportedVideoType,
          })}
          onClick={stopPropagationAndClose}
          type="button"
        />
      );
    } else {
      window.log.info('Lightbox: Unexpected content type', { contentType });

      content = (
        <button
          aria-label={i18n('unsupportedAttachment')}
          className="Lightbox__object Lightbox__unsupported Lightbox__unsupported--file"
          onClick={stopPropagationAndClose}
          type="button"
        />
      );
    }
  }

  const hasNext = !zoomed && selectedIndex < media.length - 1;
  const hasPrevious = !zoomed && selectedIndex > 0;

  return root
    ? createPortal(
        <div
          className="Lightbox Lightbox__container"
          onClick={(event: MouseEvent<HTMLDivElement>) => {
            if (containerRef && event.target !== containerRef.current) {
              return;
            }
            close();
          }}
          onKeyUp={(event: React.KeyboardEvent<HTMLDivElement>) => {
            if (
              (containerRef && event.target !== containerRef.current) ||
              event.keyCode !== 27
            ) {
              return;
            }

            close();
          }}
          ref={containerRef}
          role="presentation"
        >
          <div
            className="Lightbox__main-container"
            tabIndex={-1}
            ref={focusRef}
          >
            {!zoomed && (
              <div className="Lightbox__header">
                {getConversation ? (
                  <LightboxHeader
                    getConversation={getConversation}
                    i18n={i18n}
                    message={message}
                  />
                ) : (
                  <div />
                )}
                <div className="Lightbox__controls">
                  {onForward ? (
                    <button
                      aria-label={i18n('forwardMessage')}
                      className="Lightbox__button Lightbox__button--forward"
                      onClick={handleForward}
                      type="button"
                    />
                  ) : null}
                  {onSave ? (
                    <button
                      aria-label={i18n('save')}
                      className="Lightbox__button Lightbox__button--save"
                      onClick={handleSave}
                      type="button"
                    />
                  ) : null}
                  <button
                    aria-label={i18n('close')}
                    className="Lightbox__button Lightbox__button--close"
                    onClick={close}
                    type="button"
                  />
                </div>
              </div>
            )}
            <div
              className={classNames('Lightbox__object--container', {
                'Lightbox__object--container--zoomed': zoomed,
              })}
            >
              {content}
            </div>
            {hasPrevious && (
              <div className="Lightbox__nav-prev">
                <button
                  aria-label={i18n('previous')}
                  className="Lightbox__button Lightbox__button--previous"
                  onClick={onPrevious}
                  type="button"
                />
              </div>
            )}
            {hasNext && (
              <div className="Lightbox__nav-next">
                <button
                  aria-label={i18n('next')}
                  className="Lightbox__button Lightbox__button--next"
                  onClick={onNext}
                  type="button"
                />
              </div>
            )}
          </div>
          {!zoomed && (
            <div className="Lightbox__footer">
              {isViewOnce && videoTime ? (
                <div className="Lightbox__timestamp">
                  {formatDuration(videoTime)}
                </div>
              ) : null}
              {caption ? (
                <div className="Lightbox__caption">{caption}</div>
              ) : null}
              {media.length > 1 && (
                <div className="Lightbox__thumbnails--container">
                  <div
                    className="Lightbox__thumbnails"
                    style={{
                      marginLeft:
                        0 - (selectedIndex * 64 + selectedIndex * 8 + 32),
                    }}
                  >
                    {media.map((item, index) => (
                      <button
                        className={classNames({
                          Lightbox__thumbnail: true,
                          'Lightbox__thumbnail--selected':
                            index === selectedIndex,
                        })}
                        key={item.thumbnailObjectUrl}
                        type="button"
                        onClick={() => setSelectedIndex(index)}
                      >
                        {item.thumbnailObjectUrl ? (
                          <img
                            alt={i18n('lightboxImageAlt')}
                            src={item.thumbnailObjectUrl}
                          />
                        ) : (
                          <div className="Lightbox__thumbnail--unavailable" />
                        )}
                      </button>
                    ))}
                  </div>
                </div>
              )}
            </div>
          )}
        </div>,
        root
      )
    : null;
}

function LightboxHeader({
  getConversation,
  i18n,
  message,
}: {
  getConversation: (id: string) => ConversationType;
  i18n: LocalizerType;
  message: MessageAttributesType;
}): JSX.Element {
  const conversation = getConversation(message.conversationId);

  return (
    <div className="Lightbox__header--container">
      <div className="Lightbox__header--avatar">
        <Avatar
          acceptedMessageRequest={conversation.acceptedMessageRequest}
          avatarPath={conversation.avatarPath}
          color={conversation.color}
          conversationType={conversation.type}
          i18n={i18n}
          isMe={conversation.isMe}
          name={conversation.name}
          phoneNumber={conversation.e164}
          profileName={conversation.profileName}
          sharedGroupNames={conversation.sharedGroupNames}
          size={AvatarSize.THIRTY_TWO}
          title={conversation.title}
          unblurredAvatarPath={conversation.unblurredAvatarPath}
        />
      </div>
      <div className="Lightbox__header--content">
        <div className="Lightbox__header--name">{conversation.title}</div>
        <div className="Lightbox__header--timestamp">
          {moment(message.received_at_ms).format('L LT')}
        </div>
      </div>
    </div>
  );
}
