import {forward} from '@enact/core/handle';
import ForwardRef from '@enact/ui/ForwardRef';
import {Media, getKeyFromSource} from '@enact/ui/Media';
import EnactPropTypes from '@enact/core/internal/prop-types';
import Slottable from '@enact/ui/Slottable';
import compose from 'ramda/src/compose';
import React from 'react';

import css from './VideoPlayer.module.less';

import PropTypes from 'prop-types';

/**
 * Adds support for preloading a video source for `VideoPlayer`.
 *
 * @class VideoBase
 * @memberof sandstone/VideoPlayer
 * @ui
 * @private
 */
const VideoBase = class extends React.Component {
	static displayName = 'Video';

	static propTypes = /** @lends sandstone/VideoPlayer.Video.prototype */ {
		/**
		 * Video plays automatically.
		 *
		 * @type {Boolean}
		 * @default false
		 * @public
		 */
		autoPlay: PropTypes.bool,

		/**
		 * Video component to use.
		 *
		 * The default (`'video'`) renders an `HTMLVideoElement`. Custom video components must have
		 * a similar API structure, exposing the following APIs:
		 *
		 * Properties:
		 * * `currentTime` {Number} - Playback index of the media in seconds
		 * * `duration` {Number} - Media's entire duration in seconds
		 * * `error` {Boolean} - `true` if video playback has errored.
		 * * `loading` {Boolean} - `true` if video playback is loading.
		 * * `paused` {Boolean} - Playing vs paused state. `true` means the media is paused
		 * * `playbackRate` {Number} - Current playback rate, as a number
		 * * `proportionLoaded` {Number} - A value between `0` and `1`
		 *	representing the proportion of the media that has loaded
		 * * `proportionPlayed` {Number} - A value between `0` and `1` representing the
		 *	proportion of the media that has already been shown
		 *
		 * Events:
		 * * `onLoadStart` - Called when the video starts to load
		 * * `onPlay` - Sent when playback of the media starts after having been paused
		 * * `onUpdate` - Sent when any of the properties were updated
		 *
		 * Methods:
		 * * `play()` - play video
		 * * `pause()` - pause video
		 * * `load()` - load video
		 *
		 * The [`source`]{@link sandstone/VideoPlayer.Video.source} property is passed to
		 * the video component as a child node.
		 *
		 * @type {String|Component|Element}
		 * @default 'video'
		 * @public
		 */
		mediaComponent: EnactPropTypes.renderableOverride,

		/**
		 * The video source to be preloaded. Expects a `<source>` node.
		 *
		 * @type {Node}
		 * @public
		 */
		preloadSource:  PropTypes.node,

		/**
		 * Called with a reference to the active [Media]{@link ui/Media.Media} component.
		 *
		 * @type {Function}
		 * @private
		 */
		setMedia: PropTypes.func,

		/**
		 * The video source to be played.
		 *
		 * Any children `<source>` elements will be sent directly to the `mediaComponent` as video
		 * sources.
		 *
		 * See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/source
		 *
		 * @type {String|Node}
		 * @public
		 */
		source: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
		track: PropTypes.oneOfType([PropTypes.string, PropTypes.node])
	};

	static defaultProps = {
		mediaComponent: 'video'
	};

	componentDidUpdate (prevProps) {
		const {source, preloadSource} = this.props;
		const {source: prevSource, preloadSource: prevPreloadSource} = prevProps;

		const key = getKeyFromSource(source);
		const prevKey = getKeyFromSource(prevSource);
		const preloadKey = getKeyFromSource(preloadSource);
		const prevPreloadKey = getKeyFromSource(prevPreloadSource);

		if (this.props.setMedia !== prevProps.setMedia) {
			this.clearMedia(prevProps);
			this.setMedia();
		}

		if (source) {
			if (key === prevPreloadKey && preloadKey !== prevPreloadKey) {
				// if there's source and it was the preload source

				// if the preloaded video didn't error, notify VideoPlayer it is ready to reset
				if (this.preloadLoadStart) {
					forward('onLoadStart', this.preloadLoadStart, this.props);
				}

				// emit onUpdate to give VideoPlayer an opportunity to updates its internal state
				// since it won't receive the onLoadStart or onError event
				forward('onUpdate', {type: 'onUpdate'}, this.props);

				this.autoPlay();
			} else if (key !== prevKey) {
				// if there's source and it has changed.
				this.autoPlay();
			}
		}

		if (preloadSource && preloadKey !== prevPreloadKey) {
			this.preloadLoadStart = null;

			// In the case that the previous source equalled the previous preload (causing the
			// preload video node to not be created) and then the preload source was changed, we
			// need to guard against accessing the preloadVideo node.
			if (this.preloadVideo) {
				this.preloadVideo.load();
			}
		}
	}

	componentWillUnmount () {
		this.clearMedia();
	}

	keys = ['media-1', 'media-2'];
	prevSourceKey = null;
	prevPreloadKey = null;

	handlePreloadLoadStart = (ev) => {
		// persist the event so we can cache it to re-emit when the preload becomes active
		ev.persist();
		this.preloadLoadStart = ev;

		// prevent the from bubbling to upstream handlers
		ev.stopPropagation();
	};

	clearMedia ({setMedia} = this.props) {
		if (setMedia) {
			setMedia(null);
		}
	}

	setMedia ({setMedia} = this.props) {
		if (setMedia) {
			setMedia(this.video);
		}
	}

	autoPlay () {
		if (!this.props.autoPlay) return;

		this.video.play();
	}

	setVideoRef = (node) => {
		this.video = node;
		this.setMedia();
	};

	setPreloadRef = (node) => {
		if (node) {
			node.load();
		}
		this.preloadVideo = node;
	};

	getKeys () {
		const {source, preloadSource} = this.props;

		const sourceKey = source && getKeyFromSource(source);
		let preloadKey = preloadSource && getKeyFromSource(preloadSource);

		// If the same source is used for both, clear the preload key to avoid rendering duplicate
		// video elements.
		if (sourceKey === preloadKey) {
			preloadKey = null;
		}

		// if either the source or preload existed previously in the other "slot", swap the keys so
		// the preload video becomes the active video and vice versa
		if (
			(sourceKey === this.prevPreloadKey && this.prevPreloadKey) ||
			(preloadKey === this.prevSourceKey && this.prevSourceKey)
		) {
			this.keys.reverse();
		}

		// cache the previous keys so we know if the sources change the next time
		this.prevSourceKey = sourceKey;
		this.prevPreloadKey = preloadKey;

		// if preload is unset, clear the key so we don't render that media node at all
		return preloadKey ? this.keys : this.keys.slice(0, 1);
	}

	render () {
		const {
			preloadSource,
			source,
			track,
			mediaComponent,
			...rest
		} = this.props;

		delete rest.setMedia;

		const [sourceKey, preloadKey] = this.getKeys();

		return (
			<React.Fragment>
				{sourceKey ? (
					<Media
						{...rest}
						className={css.video}
						controls={false}
						key={sourceKey}
						mediaComponent={mediaComponent}
						preload="none"
						ref={this.setVideoRef}
						track={React.isValidElement(track) ? track : (
							<track src={track} />
						)}
						source={React.isValidElement(source) ? source : (
							<source src={source} />
						)}
					/>
				) : null}
				{preloadKey ? (
					<Media
						autoPlay={false}
						className={css.preloadVideo}
						controls={false}
						key={preloadKey}
						mediaComponent={mediaComponent}
						onLoadStart={this.handlePreloadLoadStart}
						preload="none"
						ref={this.setPreloadRef}
						track={React.isValidElement(track) ? track : (
							<track src={track} />
						)}
						source={React.isValidElement(preloadSource) ? preloadSource : (
							<source src={preloadSource} />
						)}
					/>
				) : null}
			</React.Fragment>
		);
	}
};

const VideoDecorator = compose(
	ForwardRef({prop: 'setMedia'}),
	Slottable({slots: ['source', 'track','preloadSource']})
);

/**
 * Provides support for more advanced video configurations for `VideoPlayer`.
 *
 * Custom Video Tag
 *
 * ```
 * <VideoPlayer>
 *   <Video mediaComponent="custom-video-element">
 *     <source src="path/to/source.mp4" />
 *   </Video>
 * </VideoPlayer>
 * ```
 *
 * Preload Video Source
 *
 * ```
 * <VideoPlayer>
 *   <Video>
 *     <source src="path/to/source.mp4" />
 *     <source src="path/to/preload-source.mp4" slot="preloadSource" />
 *   </Video>
 * </VideoPlayer>
 * ```
 *
 * @class Video
 * @mixes ui/Slottable.Slottable
 * @memberof sandstone/VideoPlayer
 * @ui
 * @public
 */
const Video = VideoDecorator(VideoBase);
Video.defaultSlot = 'videoComponent';

export default Video;
export {
	Video
};
