# panorama/Panorama.js

    import { Infospot } from '../infospot/Infospot';
    import { DataImage } from '../DataImage';
    import * as THREE from 'three';
    import TWEEN from '@tweenjs/tween.js';

    /**
     * @classdesc Base Panorama
     * @constructor
     * @param {THREE.Geometry} geometry - The geometry for this panorama
     * @param {THREE.Material} material - The material for this panorama
     */
    function Panorama ( geometry, material ) {

        THREE.Mesh.call( this, geometry, material );

        this.type = 'panorama';

        this.ImageQualityLow = 1;
        this.ImageQualityFair = 2;
        this.ImageQualityMedium = 3;
        this.ImageQualityHigh = 4;
        this.ImageQualitySuperHigh = 5;

        this.animationDuration = 1000;

        this.defaultInfospotSize = 350;

        this.container = undefined;

        this.loaded = false;

        this.linkedSpots = [];

        this.isInfospotVisible = false;

        this.linkingImageURL = undefined;
        this.linkingImageScale = undefined;

        this.material.side = THREE.BackSide;
        this.material.opacity = 0;

        this.scale.x *= -1;
        this.renderOrder = -1;

        this.active = false;

        this.infospotAnimation = new TWEEN.Tween( this ).to( {}, this.animationDuration / 2 );

        this.addEventListener( 'load', this.fadeIn.bind( this ) );
        this.addEventListener( 'panolens-container', this.setContainer.bind( this ) );
        this.addEventListener( 'click', this.onClick.bind( this ) );

        this.setupTransitions();

    }

    Panorama.prototype = Object.assign( Object.create( THREE.Mesh.prototype ), {

        constructor: Panorama,

        /**
         * Adding an object
         * To counter the scale.x = -1, it will automatically add an
         * empty object with inverted scale on x
         * @memberOf Panorama
         * @instance
         * @param {THREE.Object3D} object - The object to be added
         */
        add: function ( object ) {

            let invertedObject;

            if ( arguments.length > 1 ) {

                for ( var i = 0; i < arguments.length; i ++ ) {

                    this.add( arguments[ i ] );

                }

                return this;

            }

            // In case of infospots
            if ( object instanceof Infospot ) {

                invertedObject = object;

                if ( object.dispatchEvent ) {

                    const { container } = this;

                    if ( container ) { object.dispatchEvent( { type: 'panolens-container', container } ); }

                    object.dispatchEvent( { type: 'panolens-infospot-focus', method: function ( vector, duration, easing ) {

                        /**
                         * Infospot focus handler event
                         * @type {object}
                         * @event Panorama#panolens-viewer-handler
                         * @property {string} method - Viewer function name
                         * @property {*} data - The argument to be passed into the method
                         */
                        this.dispatchEvent( { type: 'panolens-viewer-handler', method: 'tweenControlCenter', data: [ vector, duration, easing ] } );

                    }.bind( this ) } );
                }

            } else {

                // Counter scale.x = -1 effect
                invertedObject = new THREE.Object3D();
                invertedObject.scale.x = -1;
                invertedObject.scalePlaceHolder = true;
                invertedObject.add( object );

            }

            THREE.Object3D.prototype.add.call( this, invertedObject );

        },

        load: function () {

            this.onLoad();

        },

        /**
         * Click event handler
         * @param  {object} event - Click event
         * @memberOf Panorama
         * @instance
         * @fires Infospot#dismiss
         */
        onClick: function ( event ) {

            if ( event.intersects && event.intersects.length === 0 ) {

                this.traverse( function ( object ) {

                    /**
                     * Dimiss event
                     * @type {object}
                     * @event Infospot#dismiss
                     */
                    object.dispatchEvent( { type: 'dismiss' } );

                } );

            }

        },

        /**
         * Set container of this panorama
         * @param {HTMLElement|object} data - Data with container information
         * @memberOf Panorama
         * @instance
         * @fires Infospot#panolens-container
         */
        setContainer: function ( data ) {

            let container;

            if ( data instanceof HTMLElement ) {

                container = data;

            } else if ( data && data.container ) {

                container = data.container;

            }

            if ( container ) {

                this.children.forEach( function ( child ) {

                    if ( child instanceof Infospot && child.dispatchEvent ) {

                        /**
                         * Set container event
                         * @type {object}
                         * @event Infospot#panolens-container
                         * @property {HTMLElement} container - The container of this panorama
                         */
                        child.dispatchEvent( { type: 'panolens-container', container: container } );

                    }

                } );

                this.container = container;

            }

        },

        /**
         * This will be called when panorama is loaded
         * @memberOf Panorama
         * @instance
         * @fires Panorama#load
         */
        onLoad: function () {

            this.loaded = true;

            /**
             * Load panorama event
             * @type {object}
             * @event Panorama#load
             */
            this.dispatchEvent( { type: 'load' } );

        },

        /**
         * This will be called when panorama is in progress
         * @memberOf Panorama
         * @instance
         * @fires Panorama#progress
         */
        onProgress: function ( progress ) {

            /**
             * Loading panorama progress event
             * @type {object}
             * @event Panorama#progress
             * @property {object} progress - The progress object containing loaded and total amount
             */
            this.dispatchEvent( { type: 'progress', progress: progress } );

        },

        /**
         * This will be called when panorama loading has error
         * @memberOf Panorama
         * @instance
         * @fires Panorama#error
         */
        onError: function () {

            /**
             * Loading panorama error event
             * @type {object}
             * @event Panorama#error
             */
            this.dispatchEvent( { type: 'error' } );

        },

        /**
         * Get zoom level based on window width
         * @memberOf Panorama
         * @instance
         * @return {number} zoom level indicating image quality
         */
        getZoomLevel: function () {

            let zoomLevel;

            if ( window.innerWidth <= 800 ) {

                zoomLevel = this.ImageQualityFair;

            } else if ( window.innerWidth > 800 &&  window.innerWidth <= 1280 ) {

                zoomLevel = this.ImageQualityMedium;

            } else if ( window.innerWidth > 1280 && window.innerWidth <= 1920 ) {

                zoomLevel = this.ImageQualityHigh;

            } else if ( window.innerWidth > 1920 ) {

                zoomLevel = this.ImageQualitySuperHigh;

            } else {

                zoomLevel = this.ImageQualityLow;

            }

            return zoomLevel;

        },

        /**
         * Update texture of a panorama
         * @memberOf Panorama
         * @instance
         * @param {THREE.Texture} texture - Texture to be updated
         */
        updateTexture: function ( texture ) {

            this.material.map = texture;
            this.material.needsUpdate = true;

        },

        /**
         * Toggle visibility of infospots in this panorama
         * @param  {boolean} isVisible - Visibility of infospots
         * @param  {number} delay - Delay in milliseconds to change visibility
         * @memberOf Panorama
         * @instance
         * @fires Panorama#infospot-animation-complete
         */
        toggleInfospotVisibility: function ( isVisible, delay ) {

            delay = ( delay !== undefined ) ? delay : 0;

            const visible = ( isVisible !== undefined ) ? isVisible : ( this.isInfospotVisible ? false : true );

            this.traverse( function ( object ) {

                if ( object instanceof Infospot ) {

                    if ( visible ) {

                        object.show( delay );

                    } else {

                        object.hide( delay );

                    }

                }

            } );

            this.isInfospotVisible = visible;

            // Animation complete event
            this.infospotAnimation.onComplete( function () {

                /**
                 * Complete toggling infospot visibility
                 * @event Panorama#infospot-animation-complete
                 * @type {object}
                 */
                this.dispatchEvent( { type: 'infospot-animation-complete', visible: visible } );

            }.bind( this ) ).delay( delay ).start();

        },

        /**
         * Set image of this panorama's linking infospot
         * @memberOf Panorama
         * @instance
         * @param {string} url   - Url to the image asset
         * @param {number} scale - Scale factor of the infospot
         */
        setLinkingImage: function ( url, scale ) {

            this.linkingImageURL = url;
            this.linkingImageScale = scale;

        },

        /**
         * Link one-way panorama
         * @param  {Panorama} pano  - The panorama to be linked to
         * @param  {THREE.Vector3} position - The position of infospot which navigates to the pano
         * @param  {number} [imageScale=300] - Image scale of linked infospot
         * @param  {string} [imageSrc=DataImage.Arrow] - The image source of linked infospot
         * @memberOf Panorama
         * @instance
         */
        link: function ( pano, position, imageScale, imageSrc ) {

            let scale, img;

            this.visible = true;

            if ( !position ) {

                console.warn( 'Please specify infospot position for linking' );

                return;

            }

            // Infospot scale
            if ( imageScale !== undefined ) {

                scale = imageScale;

            } else if ( pano.linkingImageScale !== undefined ) {

                scale = pano.linkingImageScale;

            } else {

                scale = 300;

            }

            // Infospot image
            if ( imageSrc ) {

                img = imageSrc;

            } else if ( pano.linkingImageURL ) {

                img = pano.linkingImageURL;

            } else {

                img = DataImage.Arrow;

            }

            // Creates a new infospot
            const spot = new Infospot( scale, img );
            spot.position.copy( position );
            spot.toPanorama = pano;
            spot.addEventListener( 'click', function () {

                /**
                 * Viewer handler event
                 * @type {object}
                 * @event Panorama#panolens-viewer-handler
                 * @property {string} method - Viewer function name
                 * @property {*} data - The argument to be passed into the method
                 */
                this.dispatchEvent( { type: 'panolens-viewer-handler', method: 'setPanorama', data: pano } );

            }.bind( this ) );

            this.linkedSpots.push( spot );

            this.add( spot );

            this.visible = false;

        },

        reset: function () {

            this.children.length = 0;

        },

        setupTransitions: function () {

            this.fadeInAnimation = new TWEEN.Tween( this.material )
                .easing( TWEEN.Easing.Quartic.Out )
                .onStart( function () {

                    this.visible = true;
                    // this.material.visible = true;

                    /**
                     * Enter panorama fade in start event
                     * @event Panorama#enter-fade-start
                     * @type {object}
                     */
                    this.dispatchEvent( { type: 'enter-fade-start' } );

                }.bind( this ) );

            this.fadeOutAnimation = new TWEEN.Tween( this.material )
                .easing( TWEEN.Easing.Quartic.Out )
                .onComplete( function () {

                    this.visible = false;
                    // this.material.visible = true;

                    /**
                     * Leave panorama complete event
                     * @event Panorama#leave-complete
                     * @type {object}
                     */
                    this.dispatchEvent( { type: 'leave-complete' } );

                }.bind( this ) );

            this.enterTransition = new TWEEN.Tween( this )
                .easing( TWEEN.Easing.Quartic.Out )
                .onComplete( function () {

                    /**
                     * Enter panorama and animation complete event
                     * @event Panorama#enter-complete
                     * @type {object}
                     */
                    this.dispatchEvent( { type: 'enter-complete' } );

                }.bind ( this ) )
                .start();

            this.leaveTransition = new TWEEN.Tween( this )
                .easing( TWEEN.Easing.Quartic.Out );

        },

        onFadeAnimationUpdate: function () {

            const alpha = this.material.opacity;
            const { uniforms } = this.material;

            if ( uniforms && uniforms.opacity ) {
                uniforms.opacity.value = alpha;
            }

        },

        /**
         * Start fading in animation
         * @memberOf Panorama
         * @instance
         * @fires Panorama#enter-fade-complete
         */
        fadeIn: function ( duration ) {

            duration = duration >= 0 ? duration : this.animationDuration;

            this.fadeOutAnimation.stop();
            this.fadeInAnimation
                .to( { opacity: 1 }, duration )
                .onUpdate( this.onFadeAnimationUpdate.bind( this ) )
                .onComplete( function () {

                    this.toggleInfospotVisibility( true, duration / 2 );

                    /**
                     * Enter panorama fade complete event
                     * @event Panorama#enter-fade-complete
                     * @type {object}
                     */
                    this.dispatchEvent( { type: 'enter-fade-complete' } );

                }.bind( this ) )
                .start();

        },

        /**
         * Start fading out animation
         * @memberOf Panorama
         * @instance
         */
        fadeOut: function ( duration ) {

            duration = duration >= 0 ? duration : this.animationDuration;

            this.fadeInAnimation.stop();
            this.fadeOutAnimation
                .to( { opacity: 0 }, duration )
                .onUpdate( this.onFadeAnimationUpdate.bind( this ) )
                .start();

        },

        /**
         * This will be called when entering a panorama
         * @memberOf Panorama
         * @instance
         * @fires Panorama#enter
         * @fires Panorama#enter-start
         */
        onEnter: function () {

            const duration = this.animationDuration;

            this.leaveTransition.stop();
            this.enterTransition
                .to( {}, duration )
                .onStart( function () {

                    /**
                     * Enter panorama and animation starting event
                     * @event Panorama#enter-start
                     * @type {object}
                     */
                    this.dispatchEvent( { type: 'enter-start' } );

                    if ( this.loaded ) {

                        this.fadeIn( duration );

                    } else {

                        this.load();

                    }

                }.bind( this ) )
                .start();

            /**
             * Enter panorama event
             * @event Panorama#enter
             * @type {object}
             */
            this.dispatchEvent( { type: 'enter' } );

            this.children.forEach( child => {

                child.dispatchEvent( { type: 'panorama-enter' } );

            } );

            this.active = true;

        },

        /**
         * This will be called when leaving a panorama
         * @memberOf Panorama
         * @instance
         * @fires Panorama#leave
         */
        onLeave: function () {

            const duration = this.animationDuration;

            this.enterTransition.stop();
            this.leaveTransition
                .to( {}, duration )
                .onStart( function () {

                    /**
                     * Leave panorama and animation starting event
                     * @event Panorama#leave-start
                     * @type {object}
                     */
                    this.dispatchEvent( { type: 'leave-start' } );

                    this.fadeOut( duration );
                    this.toggleInfospotVisibility( false );

                }.bind( this ) )
                .start();

            /**
             * Leave panorama event
             * @event Panorama#leave
             * @type {object}
             */
            this.dispatchEvent( { type: 'leave' } );

            this.children.forEach( child => {

                child.dispatchEvent( { type: 'panorama-leave' } );

            } );

            this.active = false;

        },

        /**
         * Dispose panorama
         * @memberOf Panorama
         * @instance
         */
        dispose: function () {

            this.infospotAnimation.stop();
            this.fadeInAnimation.stop();
            this.fadeOutAnimation.stop();
            this.enterTransition.stop();
            this.leaveTransition.stop();

            /**
             * On panorama dispose handler
             * @type {object}
             * @event Panorama#panolens-viewer-handler
             * @property {string} method - Viewer function name
             * @property {*} data - The argument to be passed into the method
             */
            this.dispatchEvent( { type: 'panolens-viewer-handler', method: 'onPanoramaDispose', data: this } );

            // recursive disposal on 3d objects
            function recursiveDispose ( object ) {

                const { geometry, material } = object;

                for ( var i = object.children.length - 1; i >= 0; i-- ) {

                    recursiveDispose( object.children[i] );
                    object.remove( object.children[i] );

                }

                if ( object instanceof Infospot ) {

                    object.dispose();

                }

                if ( geometry ) { geometry.dispose(); object.geometry = null; }
                if ( material ) { material.dispose(); object.material = null; }

            }

            recursiveDispose( this );

            if ( this.parent ) {

                this.parent.remove( this );

            }

        }

    } );

    export { Panorama };