import type {
  ElementRef } from '../lib/teact/teact';
import {
  useEffect, useMemo, useRef, useState,
} from '../lib/teact/teact';
import { getActions, getGlobal } from '../global';

import type { Track, TrackId } from '../util/audioPlayer';
import type { MediaSessionHandlers } from '../util/mediaSession';

import { PLAYBACK_RATE_FOR_AUDIO_MIN_DURATION } from '../config';
import { selectTabState } from '../global/selectors';
import { register } from '../util/audioPlayer';
import {
  registerMediaSession, setPlaybackState, setPositionState, updateMetadata,
} from '../util/mediaSession';
import { isSafariPatchInProgress } from '../util/patchSafariProgressiveAudio';
import useEffectWithPrevDeps from './useEffectWithPrevDeps';
import useLastCallback from './useLastCallback';
import useSyncEffect from './useSyncEffect';

type Handler = (e: Event) => void;

const DEFAULT_SKIP_TIME = 10;

const useAudioPlayer = (
  trackId: TrackId | undefined,
  originalDuration: number, // Sometimes incorrect for voice messages
  trackType: Track['type'],
  src?: string,
  handlers?: Record<string, Handler>,
  metadata?: MediaMetadata,
  onInit?: (element: HTMLAudioElement) => void,
  shouldPlay = false,
  onForcePlay?: NoneToVoidFunction,
  onTrackChange?: NoneToVoidFunction,
  noPlaylist = false,
  noProgressUpdates = false,
  onPause?: NoneToVoidFunction,
  noReset = false,
  noHandleEvents = false,
) => {
  const controllerRef = useRef<ReturnType<typeof register>>();

  const [isPlaying, setIsPlaying] = useState(false);
  let isPlayingSync = isPlaying;

  const [playProgress, setPlayProgress] = useState<number>(0);

  const handleTrackChange = useLastCallback(() => {
    setIsPlaying(false);
    if (onTrackChange) onTrackChange();
  });

  useSyncEffect(() => {
    if (!trackId) {
      return;
    }
    controllerRef.current = register(trackId, trackType, (eventName, e) => {
      if (noHandleEvents) {
        return;
      }
      switch (eventName) {
        case 'onPlay': {
          const {
            setVolume, setPlaybackRate, toggleMuted, proxy,
          } = controllerRef.current!;
          const global = getGlobal();

          setIsPlaying(true);
          if (trackType !== 'oneTimeVoice') {
            registerMediaSession(metadata, makeMediaHandlers(controllerRef));
          }
          setPlaybackState('playing');

          const { audioPlayer: tabAudioPlayerState } = selectTabState(global);
          const { audioPlayer: globalAudioPlayerState } = global;
          setVolume(globalAudioPlayerState.volume);
          toggleMuted(Boolean(tabAudioPlayerState.isMuted));
          const duration = proxy.duration && Number.isFinite(proxy.duration) ? proxy.duration : originalDuration;
          if (trackType === 'voice' || duration > PLAYBACK_RATE_FOR_AUDIO_MIN_DURATION) {
            setPlaybackRate(tabAudioPlayerState.playbackRate);
          }
          setPositionState({
            duration: proxy.duration || 0,
            playbackRate: proxy.playbackRate,
            position: proxy.currentTime,
          });
          break;
        }
        case 'onRateChange': {
          const { proxy } = controllerRef.current!;
          setPositionState({
            duration: proxy.duration || 0,
            playbackRate: proxy.playbackRate,
            position: proxy.currentTime,
          });
          break;
        }
        case 'onPause':
          setIsPlaying(false);
          setPlaybackState('paused');
          onPause?.();
          break;
        case 'onTimeUpdate': {
          const { proxy } = controllerRef.current!;
          if (noReset && proxy.currentTime === 0) {
            break;
          }
          const duration = proxy.duration && Number.isFinite(proxy.duration) ? proxy.duration : originalDuration;
          if (!noProgressUpdates) setPlayProgress(proxy.currentTime / duration);
          break;
        }
        case 'onEnded': {
          setPlaybackState('paused');
          break;
        }
      }
      handlers?.[eventName]?.(e);
    }, onForcePlay, handleTrackChange);

    const { proxy } = controllerRef.current;

    if (!isPlaying && !proxy.paused) {
      setIsPlaying(true);
      // `isPlayingSync` is only needed to help `setIsPlaying` because it is asynchronous
      // eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
      isPlayingSync = true;
    }

    if (onInit && !noHandleEvents) {
      onInit(proxy);
    }
  }, [trackId]);

  useEffect(() => {
    if (isPlaying && metadata) updateMetadata(metadata);
  }, [metadata, isPlaying]);

  const {
    play,
    pause,
    setCurrentTime,
    proxy,
    destroy,
    setVolume,
    stop,
    isFirst,
    isLast,
    requestNextTrack,
    requestPreviousTrack,
    setPlaybackRate,
    toggleMuted,
  } = controllerRef.current ?? {};

  const duration = useMemo(() => {
    return proxy?.duration && Number.isFinite(proxy.duration) ? proxy.duration : originalDuration;
  }, [proxy?.duration, originalDuration]);

  // RAF progress
  useEffect(() => {
    if (!proxy) {
      return;
    }
    if (noReset && proxy.currentTime === 0) {
      return;
    }
    if (duration && !isSafariPatchInProgress(proxy) && !noProgressUpdates) {
      setPlayProgress(proxy.currentTime / duration);
    }
  }, [duration, playProgress, proxy, noProgressUpdates, noReset]);

  // Cleanup
  useEffect(() => () => {
    destroy?.(noPlaylist);
  }, [destroy, noPlaylist]);

  // Autoplay once `src` is present
  useEffectWithPrevDeps(([prevShouldPlay, prevSrc]) => {
    if (prevShouldPlay === shouldPlay && src === prevSrc) {
      return;
    }

    // When paused by another player
    if (proxy?.src && proxy?.paused) {
      return;
    }

    if (shouldPlay && src && !isPlaying) {
      play?.(src);
    }
  }, [shouldPlay, src, isPlaying, play, proxy?.src, proxy?.paused, trackType]);

  const playIfPresent = useLastCallback(() => {
    if (src) {
      play?.(src);
    }
  });

  const playPause = useLastCallback(() => {
    if (isPlaying) {
      pause?.();
    } else {
      playIfPresent();
    }
  });

  const setTime = useLastCallback((time: number) => {
    setCurrentTime?.(time);
    if (duration && proxy) {
      setPlayProgress(proxy.currentTime / duration);
    }
  });

  return {
    isPlaying: isPlayingSync,
    playProgress,
    playPause,
    pause,
    play: playIfPresent,
    stop,
    setCurrentTime: setTime,
    setVolume,
    audioProxy: proxy,
    duration,
    requestNextTrack,
    requestPreviousTrack,
    isFirst,
    isLast,
    setPlaybackRate,
    toggleMuted,
  };
};

function makeMediaHandlers(controllerRef: ElementRef<ReturnType<typeof register>>) {
  let mediaHandlers: MediaSessionHandlers = {};
  if (controllerRef && controllerRef.current) {
    const {
      play, pause, setCurrentTime, proxy, requestNextTrack, requestPreviousTrack, isFirst, isLast,
    } = controllerRef.current;
    mediaHandlers = {
      play: () => {
        play(proxy.src);
      },
      pause: () => {
        pause();
      },
      stop: () => {
        pause();
        setCurrentTime(0);
        getActions().closeAudioPlayer();
      },
      seekbackward: (event) => {
        const skipTime = event.seekOffset || DEFAULT_SKIP_TIME;
        setCurrentTime(Math.max(proxy.currentTime - skipTime, 0));
      },
      seekforward: (event) => {
        const skipTime = event.seekOffset || DEFAULT_SKIP_TIME;
        setCurrentTime(Math.min(proxy.currentTime + skipTime, proxy.duration));
      },
      seekTo: (event) => {
        if (event.seekTime) {
          setCurrentTime(event.seekTime);
        }
      },
    };

    if (!isLast()) {
      mediaHandlers.nexttrack = () => {
        requestNextTrack();
      };
    }
    if (!isFirst()) {
      mediaHandlers.previoustrack = () => {
        requestPreviousTrack();
      };
    }
  }
  return mediaHandlers;
}

export default useAudioPlayer;
