/* eslint-disable jsx-a11y/media-has-caption */
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Avatar, Icon, Icons, Spinner, Text } from 'folds';
import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
import { Range } from 'react-range';
import { useTranslation } from 'react-i18next';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { IAudioInfo } from '../../../../types/matrix/common';
import {
  PlayTimeCallback,
  useMediaLoading,
  useMediaPlay,
  useMediaPlayTimeCallback,
  useMediaSeek,
} from '../../../hooks/media';
import { useThrottle } from '../../../hooks/useThrottle';
import { secondsToMinutesAndSeconds } from '../../../utils/common';
import {
  decryptFile,
  downloadEncryptedMedia,
  downloadMedia,
  mxcUrlToHttp,
} from '../../../utils/matrix';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { normalizeWaveform } from '../../../utils/audioWaveform';
import { UserAvatar } from '../../user-avatar';
import * as css from './VoiceContent.css';

const PLAY_TIME_THROTTLE_OPS = {
  wait: 200,
  immediate: true,
};

const TARGET_BARS = 40;

export type VoiceContentProps = {
  mimeType: string;
  url: string;
  info: IAudioInfo;
  encInfo?: EncryptedAttachmentInfo;
  waveform?: number[];
  // The message sender — drives the always-present avatar on the left and the
  // own-vs-other frame. Non-timeline callers (pin-menu, search) may omit them,
  // in which case the avatar falls back to a placeholder.
  senderId?: string;
  senderAvatarUrl?: string;
  senderName?: string;
  // Set when the surrounding layout already draws a per-message avatar (channel
  // layout / thread drawer) — skip our own to avoid two avatars on the row.
  hideAvatar?: boolean;
};
export function VoiceContent({
  mimeType,
  url,
  info,
  encInfo,
  waveform,
  senderId,
  senderAvatarUrl,
  senderName,
  hideAvatar,
}: VoiceContentProps) {
  const { t } = useTranslation();
  const mx = useMatrixClient();
  const useAuthentication = useMediaAuthentication();

  const [srcState, loadSrc] = useAsyncCallback(
    useCallback(async () => {
      const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication);
      if (!mediaUrl) throw new Error('Invalid media URL');
      const fileContent = encInfo
        ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
        : await downloadMedia(mediaUrl);
      return URL.createObjectURL(fileContent);
    }, [mx, url, useAuthentication, mimeType, encInfo])
  );

  // Revoke the downloaded object URL when it changes / on unmount.
  useEffect(() => {
    const objectUrl = srcState.status === AsyncStatus.Success ? srcState.data : undefined;
    return () => {
      if (objectUrl) URL.revokeObjectURL(objectUrl);
    };
  }, [srcState]);

  const audioRef = useRef<HTMLAudioElement | null>(null);

  const [currentTime, setCurrentTime] = useState(0);
  // duration in seconds. (NOTE: info.duration is in milliseconds)
  const infoDuration = info.duration ?? 0;
  const [duration, setDuration] = useState(
    (Number.isFinite(infoDuration) && infoDuration >= 0 ? infoDuration : 0) / 1000
  );

  const getAudioRef = useCallback(() => audioRef.current, []);
  const { loading } = useMediaLoading(getAudioRef);
  const { playing, setPlaying } = useMediaPlay(getAudioRef);
  const { seek } = useMediaSeek(getAudioRef);
  const handlePlayTimeCallback: PlayTimeCallback = useCallback((d, ct) => {
    // Keep the info.duration seed when the element reports a non-finite duration
    // (ogg/opus frequently reports Infinity until fully buffered) — overwriting
    // it with 0 would flatten the waveform and show 0:00.
    if (Number.isFinite(d) && d > 0) setDuration(d);
    setCurrentTime(ct);
  }, []);
  useMediaPlayTimeCallback(
    getAudioRef,
    useThrottle(handlePlayTimeCallback, PLAY_TIME_THROTTLE_OPS)
  );

  const isOwn = !!senderId && senderId === mx.getUserId();
  const bars = useMemo(() => normalizeWaveform(waveform, TARGET_BARS), [waveform]);
  const max = duration || 1;
  const progress = duration > 0 ? Math.min(1, currentTime / duration) : 0;
  const clampedValue = Math.min(Math.max(currentTime, 0), max);

  const handlePlay = () => {
    if (srcState.status === AsyncStatus.Success) {
      setPlaying(!playing);
    } else if (srcState.status !== AsyncStatus.Loading) {
      loadSrc();
    }
  };

  const isLoading = srcState.status === AsyncStatus.Loading || loading;
  // Show elapsed once the user is into playback, otherwise the total length.
  const displayTime = playing || currentTime > 0 ? currentTime : duration;

  return (
    <div
      className={`${css.Row} ${isOwn && !hideAvatar ? css.RowOwn : ''}`}
      // The voice card runs its own play/seek interactions (the waveform track
      // isn't a button/slider element). Mark the whole card so the bubble-DM
      // tap-to-open-rail handler skips it (seeking must not also toggle the rail).
      data-no-rail-toggle="true"
    >
      {!hideAvatar && (
        <Avatar className={css.AvatarSlot} size="300">
          <UserAvatar
            userId={senderId ?? ''}
            src={senderAvatarUrl}
            alt={senderName ?? senderId ?? ''}
            renderFallback={() => <Icon size="200" src={Icons.User} filled />}
          />
        </Avatar>
      )}

      <div className={`${css.Bubble} ${isOwn ? css.BubbleOwn : ''}`}>
        <button
          type="button"
          className={`${css.PlayButton} ${playing ? css.PlayButtonPlaying : ''}`}
          onClick={handlePlay}
          disabled={srcState.status === AsyncStatus.Loading}
          aria-label={playing ? t('Room.voice_pause') : t('Room.voice_play')}
        >
          {isLoading ? (
            <Spinner variant="Secondary" size="200" />
          ) : (
            <Icon src={playing ? Icons.Pause : Icons.Play} size="300" filled />
          )}
        </button>

        <Range
          step={0.05}
          min={0}
          max={max}
          values={[clampedValue]}
          onChange={(values) => seek(values[0])}
          renderTrack={({ props, children }) =>
            bars.length > 0 ? (
              <div {...props} className={css.Waveform}>
                {bars.map((amp, index) => {
                  const played = duration > 0 && (index + 0.5) / bars.length <= progress;
                  return (
                    <span
                      // eslint-disable-next-line react/no-array-index-key
                      key={index}
                      className={`${css.WaveBar} ${played ? css.WaveBarPlayed : css.WaveBarRest}`}
                      style={{ height: `${Math.max(10, Math.round(amp * 100))}%` }}
                    />
                  );
                })}
                {children}
              </div>
            ) : (
              <div {...props} className={css.ProgressTrack}>
                <div className={css.ProgressFill} style={{ width: `${progress * 100}%` }} />
                {children}
              </div>
            )
          }
          renderThumb={({ props }) => <div {...props} className={css.WaveThumb} />}
        />

        <Text className={css.Time} size="T200">
          {secondsToMinutesAndSeconds(displayTime)}
        </Text>

        <audio controls={false} autoPlay ref={audioRef} style={{ display: 'none' }}>
          {srcState.status === AsyncStatus.Success && (
            <source src={srcState.data} type={mimeType} />
          )}
        </audio>
      </div>
    </div>
  );
}
