/* global _wpmejsSettings, MediaElementPlayer */

import { speak } from '@wordpress/a11y';
import { useEffect, useRef } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import debounce from 'debounce';
import { STATE_PLAYING, STATE_PAUSED, STATE_ERROR } from '../../../store/media-source/constants';

import './style.scss';

// MediaElement global settings.
const meJsSettings = typeof _wpmejsSettings !== 'undefined' ? _wpmejsSettings : {};

function createJumpButton( containerClass, label, clickHandler ) {
	const buttonContainer = document.createElement( 'div' );
	buttonContainer.className = containerClass;

	const button = document.createElement( 'button' );
	button.innerText = label;
	button.addEventListener( 'click', clickHandler );
	button.setAttribute( 'aria-label', label );
	button.setAttribute( 'title', label );
	buttonContainer.appendChild( button );
	return buttonContainer;
}

function AudioPlayer( {
	trackSource,
	onPlay,
	onPause,
	onError,
	onTimeChange,
	onSkipForward,
	onJumpBack,
	currentTime,
	playStatus = STATE_PAUSED,
	onMetadataLoaded,
	loadWhenReady = false,
	preload = 'metadata',
} ) {
	const audioRef = useRef( undefined );

	/**
	 * Play current audio.
	 *
	 * @public
	 */
	const play = () => {
		// Ignoring exceptions as they are handled globally from the audio element.
		audioRef.current.play().catch( () => {} );
	};

	/**
	 * Pause current audio.
	 *
	 * @public
	 */
	const pause = () => {
		audioRef.current.pause();
		speak( __( 'Paused', 'jetpack' ), 'assertive' );
	};

	// Hacky patch to MediaElementPlayer to avoid the null reference error that can happen as the
	// player is mounted and unmounted.
	useEffect( () => {
		if ( ! MediaElementPlayer.prototype._setResponsiveMode ) {
			MediaElementPlayer.prototype._setResponsiveMode =
				MediaElementPlayer.prototype.setResponsiveMode;
			MediaElementPlayer.prototype.setResponsiveMode = function () {
				const t = this;
				if ( t.getElement( t.container ).parentNode ) {
					t._setResponsiveMode();
				}
			};
		}
	}, [] );

	useEffect( () => {
		const audio = audioRef.current;

		// Pre load audio meta data.
		audio.preload = preload;

		// Insert player into the DOM.
		const mediaElement = new MediaElementPlayer( audio, {
			...meJsSettings,
			success: () => loadWhenReady && audio?.load(),
		} );

		// Add the skip and jump buttons if needed
		if ( onJumpBack || onSkipForward ) {
			const containerClass = `${ mediaElement.options.classPrefix }button ${ mediaElement.options.classPrefix }jump-button`;

			if ( onJumpBack ) {
				const buttonClass = `${ containerClass } ${ mediaElement.options.classPrefix }jump-backward-button`;
				mediaElement.addControlElement(
					createJumpButton( buttonClass, __( 'Jump Back', 'jetpack' ), onJumpBack ),
					'jumpBackwardButton'
				);
			}

			if ( onSkipForward ) {
				const buttonClass = `${ containerClass } ${ mediaElement.options.classPrefix }skip-forward-button`;
				mediaElement.addControlElement(
					createJumpButton( buttonClass, __( 'Skip Forward', 'jetpack' ), onSkipForward ),
					'skipForwardButton'
				);
			}
		}
		onPlay && audio.addEventListener( 'play', onPlay );
		onPause && audio.addEventListener( 'pause', onPause );
		onError && audio.addEventListener( 'error', onError );
		onMetadataLoaded && audio.addEventListener( 'loadedmetadata', onMetadataLoaded );

		return () => {
			// Cleanup.
			mediaElement.remove();
			onPlay && audio.removeEventListener( 'play', onPlay );
			onPause && audio.removeEventListener( 'pause', onPause );
			onError && audio.removeEventListener( 'error', onError );
			onMetadataLoaded && audio.removeEventListener( 'loadedmetadata', onMetadataLoaded );
		};
	}, [
		onPlay,
		onPause,
		onError,
		onJumpBack,
		onSkipForward,
		onMetadataLoaded,
		loadWhenReady,
		preload,
	] );

	// If we get lots of events from clicking on the progress bar in the MediaElement
	// then we can get stuck in a loop. We can so by debouncing here we wait until the
	// next tick before acting on the playStatus prop value changing.
	useEffect( () => {
		// Get the current status of the audio element and the required action to toggle it.
		const [ audioStatus, action ] =
			audioRef.current?.paused === false ? [ STATE_PLAYING, pause ] : [ STATE_PAUSED, play ];
		const debouncedAction = debounce( action, 100 );
		if ( STATE_ERROR !== playStatus && audioStatus !== playStatus ) {
			debouncedAction();
		}
		return () => {
			debouncedAction.clear();
		};
	}, [ audioRef, playStatus, trackSource ] );

	useEffect( () => {
		if ( ! onTimeChange ) {
			return;
		}
		//Add time change event listener
		const audio = audioRef.current;

		// Equivalent to lodash `throttle( time => onTimeChange( time ), 1000, { leading: true, trailing: false } );`
		// (doing `trailing: true` would be more complex)
		let lastTimeChange = 0;
		const throttledTimeChange = time => {
			const now = Date.now();
			if ( now - lastTimeChange >= 1000 ) {
				onTimeChange( time );
				lastTimeChange = now;
			}
		};

		const onTimeUpdate = e => throttledTimeChange( e.target.currentTime );
		onTimeChange && audio?.addEventListener( 'timeupdate', onTimeUpdate );

		return () => {
			// Equivalent to lodash `throttledTimeChange.cancel();`
			lastTimeChange = 0;
			audio?.removeEventListener( 'timeupdate', onTimeUpdate );
		};
	}, [ audioRef, onTimeChange ] );

	//Check current time against prop and potentially jump
	useEffect( () => {
		const audio = audioRef.current;

		// If there's no audio component or we're not controlling time with the `currentTime` prop,
		// then bail early.
		if ( ! currentTime || ! audio ) {
			return;
		}

		// We only want to change the play position if the difference between our current play position
		// and the prop is greater than 1. We're throttling the time change events to once per second, so
		// if the floored time has changed by more than a second, we haven't received an event in the past
		// two seconds. That's unlikely and so a change of more than a second should be as a result of us
		// wanting to update the position, so we set the audio element's current time as a result.
		if ( Math.abs( Math.floor( currentTime - audio.currentTime ) ) > 1 ) {
			audio.currentTime = currentTime;
		}
	}, [ audioRef, currentTime ] );

	return (
		<div className="jetpack-audio-player">
			<audio src={ trackSource } ref={ audioRef }></audio>
		</div>
	);
}

export default AudioPlayer;
