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

import React, { useRef, useState, useEffect } from 'react';
import classNames from 'classnames';
import { Blurhash } from 'react-blurhash';
import formatFileSize from 'filesize';

import { LocalizerType, ThemeType } from '../../types/Util';
import { Spinner } from '../Spinner';

import {
  AttachmentType,
  hasNotDownloaded,
  getImageDimensions,
  defaultBlurHash,
} from '../../types/Attachment';

const MAX_GIF_REPEAT = 4;
const MAX_GIF_TIME = 8;

export type Props = {
  readonly attachment: AttachmentType;
  readonly size?: number;
  readonly tabIndex: number;

  readonly i18n: LocalizerType;
  readonly theme?: ThemeType;

  readonly reducedMotion?: boolean;

  onError(): void;
  kickOffAttachmentDownload(): void;
};

type MediaEvent = React.SyntheticEvent<HTMLVideoElement, Event>;

export const GIF: React.FC<Props> = props => {
  const {
    attachment,
    size,
    tabIndex,

    i18n,
    theme,

    reducedMotion = Boolean(
      window.Accessibility && window.Accessibility.reducedMotionSetting
    ),

    onError,
    kickOffAttachmentDownload,
  } = props;

  const tapToPlay = reducedMotion;

  const videoRef = useRef<HTMLVideoElement | null>(null);
  const { height, width } = getImageDimensions(attachment, size);

  const [repeatCount, setRepeatCount] = useState(0);
  const [playTime, setPlayTime] = useState(MAX_GIF_TIME);
  const [currentTime, setCurrentTime] = useState(0);
  const [isFocused, setIsFocused] = useState(true);
  const [isPlaying, setIsPlaying] = useState(!tapToPlay);

  useEffect(() => {
    const onFocus = () => setIsFocused(true);
    const onBlur = () => setIsFocused(false);

    window.addEventListener('focus', onFocus);
    window.addEventListener('blur', onBlur);
    return () => {
      window.removeEventListener('focus', onFocus);
      window.removeEventListener('blur', onBlur);
    };
  });

  //
  // Play & Pause video in response to change of `isPlaying` and `repeatCount`.
  //
  useEffect(() => {
    const { current: video } = videoRef;
    if (!video) {
      return;
    }

    if (isPlaying) {
      video.play().catch(error => {
        window.log.info(
          "Failed to match GIF playback to window's state",
          (error && error.stack) || error
        );
      });
    } else {
      video.pause();
    }
  }, [isPlaying, repeatCount]);

  //
  // Change `isPlaying` in response to focus, play time, and repeat count
  // changes.
  //
  useEffect(() => {
    const { current: video } = videoRef;
    if (!video) {
      return;
    }

    let isTapToPlayPaused = false;
    if (tapToPlay) {
      if (
        playTime + currentTime >= MAX_GIF_TIME ||
        repeatCount >= MAX_GIF_REPEAT
      ) {
        isTapToPlayPaused = true;
      }
    }

    setIsPlaying(isFocused && !isTapToPlayPaused);
  }, [isFocused, playTime, currentTime, repeatCount, tapToPlay]);

  const onTimeUpdate = async (event: MediaEvent): Promise<void> => {
    const { currentTime: reportedTime } = event.currentTarget;
    if (!Number.isNaN(reportedTime)) {
      setCurrentTime(reportedTime);
    }
  };

  const onEnded = async (event: MediaEvent): Promise<void> => {
    const { currentTarget: video } = event;
    const { duration } = video;

    setRepeatCount(repeatCount + 1);
    if (!Number.isNaN(duration)) {
      video.currentTime = 0;

      setCurrentTime(0);
      setPlayTime(playTime + duration);
    }
  };

  const onOverlayClick = (event: React.MouseEvent): void => {
    event.preventDefault();
    event.stopPropagation();

    if (!attachment.url) {
      kickOffAttachmentDownload();
    } else if (tapToPlay) {
      setPlayTime(0);
      setCurrentTime(0);
      setRepeatCount(0);
    }
  };

  const onOverlayKeyDown = (event: React.KeyboardEvent): void => {
    if (event.key !== 'Enter' && event.key !== 'Space') {
      return;
    }

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

    kickOffAttachmentDownload();
  };

  const isPending = Boolean(attachment.pending);
  const isNotDownloaded = hasNotDownloaded(attachment) && !isPending;

  let fileSize: JSX.Element | undefined;
  if (isNotDownloaded && attachment.fileSize) {
    fileSize = (
      <div className="module-image--gif__filesize">
        {formatFileSize(attachment.fileSize || 0)} · GIF
      </div>
    );
  }

  let gif: JSX.Element | undefined;
  if (isNotDownloaded || isPending) {
    gif = (
      <Blurhash
        hash={attachment.blurHash || defaultBlurHash(theme)}
        width={width}
        height={height}
        style={{ display: 'block' }}
      />
    );
  } else {
    gif = (
      <video
        ref={videoRef}
        onTimeUpdate={onTimeUpdate}
        onEnded={onEnded}
        onError={onError}
        className="module-image--gif__video"
        autoPlay
        playsInline
        muted
        poster={attachment.screenshot && attachment.screenshot.url}
        height={height}
        width={width}
        src={attachment.url}
      />
    );
  }

  let overlay: JSX.Element | undefined;
  if ((tapToPlay && !isPlaying) || isNotDownloaded) {
    const className = classNames([
      'module-image__border-overlay',
      'module-image__border-overlay--with-click-handler',
      'module-image--soft-corners',
      isNotDownloaded
        ? 'module-image--not-downloaded'
        : 'module-image--tap-to-play',
    ]);

    overlay = (
      <button
        type="button"
        className={className}
        onClick={onOverlayClick}
        onKeyDown={onOverlayKeyDown}
        tabIndex={tabIndex}
      >
        <span />
      </button>
    );
  }

  let spinner: JSX.Element | undefined;
  if (isPending) {
    spinner = (
      <div className="module-image__download-pending--spinner-container">
        <div
          className="module-image__download-pending--spinner"
          title={i18n('loading')}
        >
          <Spinner moduleClassName="module-image-spinner" svgSize="small" />
        </div>
      </div>
    );
  }

  return (
    <div className="module-image module-image--gif">
      {gif}
      {overlay}
      {spinner}
      {fileSize}
    </div>
  );
};
