# media/Media.js

    import * as THREE from 'three';

    /**
     * @classdesc User Media
     * @constructor
     * @param {object} [constraints={ video: { width: { ideal: 1920 }, height: { ideal: 1080 }, facingMode: { exact: 'environment' } }, audio: false }]
     */
    function Media ( constraints ) {

        const defaultConstraints = { video: { width: { ideal: 1920 }, height: { ideal: 1080 }, facingMode: { exact: 'environment' } }, audio: false };

        this.constraints = Object.assign( defaultConstraints, constraints );

        this.container = null;
        this.scene = null;
        this.element = null;
        this.devices = [];
        this.stream = null;
        this.ratioScalar = 1;
        this.videoDeviceIndex = 0;

    };

    Media.prototype = Object.assign( Object.create( THREE.EventDispatcher.prototype ), {

        setContainer: function ( container ) {

            this.container = container;

        },

        setScene: function ( scene ) {

            this.scene = scene;

        },

        /**
         * Enumerate devices
         * @memberOf Media
         * @instance
         * @returns {Promise}
         */
        enumerateDevices: function () {

            const devices = this.devices;
            const resolvedPromise = new Promise( resolve => { resolve( devices ); } );

            return devices.length > 0 ? resolvedPromise : window.navigator.mediaDevices.enumerateDevices();

        },

        /**
         * Switch to next available video device
         * @memberOf Media
         * @instance
         */
        switchNextVideoDevice: function () {

            const stop = this.stop.bind( this );
            const start = this.start.bind( this );
            const setVideDeviceIndex = this.setVideDeviceIndex.bind( this );

            let index = this.videoDeviceIndex;

            this.getDevices( 'video' )
                .then( devices => {
                    stop();
                    index++;
                    if ( index >= devices.length ) {
                        setVideDeviceIndex( 0 );
                        index--;
                    } else {
                        setVideDeviceIndex( index );
                    }

                    start( devices[ index ] );

                } );

        },

        /**
         * Get devices
         * @param {string} type - type keyword to match device.kind
         * @memberOf Media
         * @instance
         */
        getDevices: function ( type = 'video' ) {

            const devices = this.devices;
            const validate = _devices => {

                return _devices.map( device => {

                    if ( !devices.includes( device ) ) { devices.push( device ); }
                    return device;

                } );

            };
            const filter = _devices => {

                const reg = new RegExp( type, 'i' );
                return _devices.filter( device => reg.test( device.kind ) );

            };

            return this.enumerateDevices()
                .then( validate )
                .then( filter );

        },

        /**
         * Get user media
         * @param {MediaStreamConstraints} constraints
         * @memberOf Media
         * @instance
         */
        getUserMedia: function ( constraints ) {

            const setMediaStream = this.setMediaStream.bind( this );
            const playVideo = this.playVideo.bind( this );
            const onCatchError = error => { console.warn( `PANOLENS.Media: ${error}` ); };

            return window.navigator.mediaDevices.getUserMedia( constraints )
                .then( setMediaStream )
                .then( playVideo )
                .catch( onCatchError );

        },

        /**
         * Set video device index
         * @param {number} index
         * @memberOf Media
         * @instance
         */
        setVideDeviceIndex: function ( index ) {

            this.videoDeviceIndex = index;

        },

        /**
         * Start streaming
         * @param {MediaDeviceInfo} [targetDevice]
         * @memberOf Media
         * @instance
         */
        start: function( targetDevice ) {

            const constraints = this.constraints;
            const getUserMedia = this.getUserMedia.bind( this );
            const onVideoDevices = devices => {

                if ( !devices || devices.length === 0 ) {

                    throw Error( 'no video device found' );

                }

                const device = targetDevice || devices[ 0 ];
                constraints.video.deviceId = device.deviceId;

                return getUserMedia( constraints );

            };

            this.element = this.createVideoElement();

            return this.getDevices().then( onVideoDevices );

        },

        /**
         * Stop streaming
         * @memberOf Media
         * @instance
         */
        stop: function () {

            const stream = this.stream;

            if ( stream && stream.active ) {

                const track = stream.getTracks()[ 0 ];

                track.stop();

                window.removeEventListener( 'resize', this.onWindowResize.bind( this ) );

                this.element = null;
                this.stream = null;

            }

        },

        /**
         * Set media stream
         * @param {MediaStream} stream
         * @memberOf Media
         * @instance
         */
        setMediaStream: function ( stream ) {

            this.stream = stream;
            this.element.srcObject = stream;

            if ( this.scene ) {

                this.scene.background = this.createVideoTexture();

            }

            window.addEventListener( 'resize', this.onWindowResize.bind( this ) );

        },

        /**
         * Play video element
         * @memberOf Media
         * @instance
         */
        playVideo: function () {

            const { element } = this;

            if ( element ) {

                element.play();
                this.dispatchEvent( { type: 'play' } );

            }

        },

        /**
         * Pause video element
         * @memberOf Media
         * @instance
         */
        pauseVideo: function () {

            const { element } = this;

            if ( element ) {

                element.pause();
                this.dispatchEvent( { type: 'pause' } );

            }

        },

        /**
         * Create video texture
         * @memberOf Media
         * @instance
         * @returns {THREE.VideoTexture}
         */
        createVideoTexture: function () {

            const video = this.element;
            const texture = new THREE.VideoTexture( video );

            texture.generateMipmaps = false;
            texture.minFilter = THREE.LinearFilter;
            texture.magFilter = THREE.LinearFilter;
            texture.format = THREE.RGBFormat;
            texture.center.set( 0.5, 0.5 );

            video.addEventListener( 'canplay', this.onWindowResize.bind( this ) );

            return texture;

        },

        /**
         * Create video element
         * @memberOf Media
         * @instance
         * @returns {HTMLVideoElement}
         * @fires Media#canplay
         */
        createVideoElement: function() {

            const dispatchEvent = this.dispatchEvent.bind( this );
            const video = document.createElement( 'video' );

            /**
             * Video can play event
             * @type {object}
             * @event Media#canplay
             */
            const canPlay = () => dispatchEvent( { type: 'canplay' } );

            video.setAttribute( 'autoplay', '' );
            video.setAttribute( 'muted', '' );
            video.setAttribute( 'playsinline', '' );

            video.style.position = 'absolute';
            video.style.top = '0';
            video.style.left = '0';
            video.style.width = '100%';
            video.style.height = '100%';
            video.style.objectPosition = 'center';
            video.style.objectFit = 'cover';
            video.style.display = this.scene ? 'none' : '';

            video.addEventListener( 'canplay', canPlay );

            return video;

        },

        /**
         * On window resize event
         * @param {Event} event
         * @memberOf Media
         * @instance
         */
        onWindowResize: function () {

            if ( this.element && this.element.videoWidth && this.element.videoHeight && this.scene ) {

                const { clientWidth: width, clientHeight: height } = this.container;
                const texture = this.scene.background;
                const { videoWidth, videoHeight } = this.element;
                const cameraRatio = videoHeight / videoWidth;
                const viewportRatio = this.container ? width / height : 1.0;
                const ratio = cameraRatio * viewportRatio * this.ratioScalar;

                if ( width > height ) {
                    texture.repeat.set( ratio, 1 );
                } else {
                    texture.repeat.set( 1, 1 / ratio );
                }

            }

        }

    } );

    export { Media };