// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { CSSProperties, ForwardedRef } from 'react';
import React, { forwardRef, useEffect, useRef, useState } from 'react';
import { useReducedMotion } from '@react-spring/web';
import { SpinnerV2 } from '../SpinnerV2';
import { strictAssert } from '../../util/assert';
import type { Loadable } from '../../util/loadable';
import { LoadingState } from '../../util/loadable';
import { useIntent } from './base/FunImage';
import { createLogger } from '../../logging/log';
import * as Errors from '../../types/errors';
import { isAbortError } from '../../util/isAbortError';

const log = createLogger('FunGif');

export type FunGifProps = Readonly<{
  src: string;
  width: number;
  height: number;
  'aria-label'?: string;
  'aria-describedby': string;
  ignoreReducedMotion?: boolean;
}>;

export function FunGif(props: FunGifProps): JSX.Element {
  if (props.ignoreReducedMotion) {
    return <FunGifBase {...props} autoPlay />;
  }
  return <FunGifReducedMotion {...props} />;
}

/** @internal */
const FunGifBase = forwardRef(function FunGifBase(
  props: FunGifProps & { autoPlay: boolean },
  ref: ForwardedRef<HTMLVideoElement>
) {
  return (
    <video
      ref={ref}
      className="FunGif"
      src={props.src}
      width={props.width}
      height={props.height}
      loop
      autoPlay={props.autoPlay}
      playsInline
      muted
      disablePictureInPicture
      disableRemotePlayback
      aria-label={props['aria-label']}
      aria-describedby={props['aria-describedby']}
    />
  );
});

/** @internal */
function FunGifReducedMotion(props: FunGifProps) {
  const videoRef = useRef<HTMLVideoElement>(null);
  const intent = useIntent(videoRef);
  const reducedMotion = useReducedMotion();
  const shouldPlay = !reducedMotion || intent;

  useEffect(() => {
    strictAssert(videoRef.current, 'Expected video element');
    const video = videoRef.current;
    if (shouldPlay) {
      video.play().catch(error => {
        // ignore errors where `play()` was interrupted by `pause()`
        if (!isAbortError(error)) {
          log.error('Playback error', Errors.toLogFormat(error));
        }
      });
    } else {
      video.pause();
    }
  }, [shouldPlay]);

  return <FunGifBase {...props} ref={videoRef} autoPlay={shouldPlay} />;
}

export type FunGifPreviewLoadable = Loadable<string>;

export type FunGifPreviewProps = Readonly<{
  src: string | null;
  state: LoadingState;
  width: number;
  height: number;
  // It would be nice if this were determined by the container, but that's a
  // difficult problem because it creates a cycle where the parent's height
  // depends on its children, and its children's height depends on its parent.
  // As far as I was able to figure out, this could only be done in one dimension
  // at a time.
  maxHeight: number;
  'aria-label'?: string;
  'aria-describedby': string;
}>;

export function FunGifPreview(props: FunGifPreviewProps): JSX.Element {
  const ref = useRef<HTMLVideoElement>(null);
  const [spinner, setSpinner] = useState(false);
  const [playbackError, setPlaybackError] = useState(false);

  const timerRef = useRef<ReturnType<typeof setTimeout>>();

  useEffect(() => {
    const timer = setTimeout(() => {
      setSpinner(true);
    });
    timerRef.current = timer;
    return () => {
      clearTimeout(timer);
    };
  }, []);

  useEffect(() => {
    if (props.src == null) {
      return;
    }
    strictAssert(ref.current != null, 'video ref should not be null');
    const video = ref.current;
    function onCanPlay() {
      video.hidden = false;
      clearTimeout(timerRef.current);
      setSpinner(false);
      setPlaybackError(false);
    }
    function onError() {
      clearTimeout(timerRef.current);
      setSpinner(false);
      setPlaybackError(true);
    }
    video.addEventListener('canplay', onCanPlay, { once: true });
    video.addEventListener('error', onError, { once: true });
    return () => {
      video.removeEventListener('canplay', onCanPlay);
      video.removeEventListener('error', onError);
    };
  }, [props.src]);

  const hasError = props.state === LoadingState.LoadFailed || playbackError;

  return (
    <div className="FunGifPreview">
      <svg
        aria-hidden
        className="FunGifPreview__Sizer"
        width={props.width}
        height={props.height}
        style={
          {
            '--fun-gif-preview-sizer-max-height': `${props.maxHeight}px`,
          } as CSSProperties
        }
      />
      <div className="FunGifPreview__Backdrop" role="status">
        {spinner && !hasError && (
          <SpinnerV2
            className="FunGifPreview__Spinner"
            size={36}
            strokeWidth={4}
          />
        )}
        {hasError && <div className="FunGifPreview__ErrorIcon" />}
      </div>
      {props.src != null && (
        <video
          ref={ref}
          className="FunGifPreview__Video"
          src={props.src}
          width={props.width}
          height={props.height}
          loop
          autoPlay
          playsInline
          muted
          disablePictureInPicture
          disableRemotePlayback
          aria-label={props['aria-label']}
          aria-describedby={props['aria-describedby']}
        />
      )}
    </div>
  );
}
