# infospot/Infospot.js

    import * as THREE from 'three';
    import { DataImage } from '../DataImage';
    import { MODES } from '../Constants';
    import { TextureLoader } from '../loaders/TextureLoader';
    import TWEEN from '@tweenjs/tween.js';

    /**
     * @classdesc Information spot attached to panorama
     * @constructor
     * @param {number} [scale=300] - Default scale
     * @param {string} [imageSrc=PANOLENS.DataImage.Info] - Image overlay info
     * @param {boolean} [animated=true] - Enable default hover animation
     */
    function Infospot ( scale = 300, imageSrc, animated ) {

        const duration = 500, scaleFactor = 1.3;

        imageSrc = imageSrc || DataImage.Info;

        THREE.Sprite.call( this );

        this.type = 'infospot';

        this.animated = animated !== undefined ? animated : true;
        this.isHovering = false;

        /*
         * TODO: Three.js bug hotfix for sprite raycasting r104
         * https://github.com/mrdoob/three.js/issues/14624
         */
        this.frustumCulled = false;

        this.element = null;
        this.toPanorama = null;
        this.cursorStyle = null;

        this.mode = MODES.NORMAL;

        this.scale.set( scale, scale, 1 );
        this.rotation.y = Math.PI;

        this.container = null;

        this.originalRaycast = this.raycast;

        // Event Handler
        this.HANDLER_FOCUS = null;

        this.material.side = THREE.DoubleSide;
        this.material.depthTest = false;
        this.material.transparent = true;
        this.material.opacity = 0;

        this.scaleUpAnimation = new TWEEN.Tween();
        this.scaleDownAnimation = new TWEEN.Tween();

        const postLoad = function ( texture ) {

            if ( !this.material ) { return; }

            const ratio = texture.image.width / texture.image.height;
            const textureScale = new THREE.Vector3();

            texture.image.width = texture.image.naturalWidth || 64;
            texture.image.height = texture.image.naturalHeight || 64;

            this.scale.set( ratio * scale, scale, 1 );

            textureScale.copy( this.scale );

            this.scaleUpAnimation = new TWEEN.Tween( this.scale )
                .to( { x: textureScale.x * scaleFactor, y: textureScale.y * scaleFactor }, duration )
                .easing( TWEEN.Easing.Elastic.Out );

            this.scaleDownAnimation = new TWEEN.Tween( this.scale )
                .to( { x: textureScale.x, y: textureScale.y }, duration )
                .easing( TWEEN.Easing.Elastic.Out );

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

        }.bind( this );

        // Add show and hide animations
        this.showAnimation = new TWEEN.Tween( this.material )
            .to( { opacity: 1 }, duration )
            .onStart( this.enableRaycast.bind( this, true ) )
            .easing( TWEEN.Easing.Quartic.Out );

        this.hideAnimation = new TWEEN.Tween( this.material )
            .to( { opacity: 0 }, duration )
            .onStart( this.enableRaycast.bind( this, false ) )
            .easing( TWEEN.Easing.Quartic.Out );

        // Attach event listeners
        this.addEventListener( 'click', this.onClick );
        this.addEventListener( 'hover', this.onHover );
        this.addEventListener( 'hoverenter', this.onHoverStart );
        this.addEventListener( 'hoverleave', this.onHoverEnd );
        this.addEventListener( 'panolens-dual-eye-effect', this.onDualEyeEffect );
        this.addEventListener( 'panolens-container', this.setContainer.bind( this ) );
        this.addEventListener( 'dismiss', this.onDismiss );
        this.addEventListener( 'panolens-infospot-focus', this.setFocusMethod );

        TextureLoader.load( imageSrc, postLoad );

    };

    Infospot.prototype = Object.assign( Object.create( THREE.Sprite.prototype ), {

        constructor: Infospot,

        /**
         * Set infospot container
         * @param {HTMLElement|object} data - Data with container information
         * @memberOf Infospot
         * @instance
         */
        setContainer: function ( data ) {

            let container;

            if ( data instanceof HTMLElement ) {

                container = data;

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

                container = data.container;

            }

            // Append element if exists
            if ( container && this.element ) {

                container.appendChild( this.element );

            }

            this.container = container;

        },

        /**
         * Get container
         * @memberOf Infospot
         * @instance
         * @return {HTMLElement} - The container of this infospot
         */
        getContainer: function () {

            return this.container;

        },

        /**
         * This will be called by a click event
         * Translate and lock the hovering element if any
         * @param  {object} event - Event containing mouseEvent with clientX and clientY
         * @memberOf Infospot
         * @instance
         */
        onClick: function ( event ) {

            if ( this.element && this.getContainer() ) {

                this.onHoverStart( event );

                // Lock element
                this.lockHoverElement();

            }

        },

        /**
         * Dismiss current element if any
         * @param  {object} event - Dismiss event
         * @memberOf Infospot
         * @instance
         */
        onDismiss: function () {

            if ( this.element ) {

                this.unlockHoverElement();
                this.onHoverEnd();

            }

        },

        /**
         * This will be called by a mouse hover event
         * Translate the hovering element if any
         * @param  {object} event - Event containing mouseEvent with clientX and clientY
         * @memberOf Infospot
         * @instance
         */
        onHover: function () {},

        /**
         * This will be called on a mouse hover start
         * Sets cursor style to 'pointer', display the element and scale up the infospot
         * @param {object} event
         * @memberOf Infospot
         * @instance
         */
        onHoverStart: function ( event ) {

            if ( !this.getContainer() ) { return; }

            const cursorStyle = this.cursorStyle || ( this.mode === MODES.NORMAL ? 'pointer' : 'default' );
            const { scaleDownAnimation, scaleUpAnimation, element } = this;

            this.isHovering = true;
            this.container.style.cursor = cursorStyle;

            if ( this.animated ) {

                scaleDownAnimation.stop();
                scaleUpAnimation.start();

            }

            if ( element && event.mouseEvent.clientX >= 0 && event.mouseEvent.clientY >= 0 ) {

                const { left, right, style } = element;

                if ( this.mode === MODES.CARDBOARD || this.mode === MODES.STEREO ) {

                    style.display = 'none';
                    left.style.display = 'block';
                    right.style.display = 'block';

                    // Store element width for reference
                    element._width = left.clientWidth;
                    element._height = left.clientHeight;

                } else {

                    style.display = 'block';
                    if ( left ) { left.style.display = 'none'; }
                    if ( right ) { right.style.display = 'none'; }

                    // Store element width for reference
                    element._width = element.clientWidth;
                    element._height = element.clientHeight;

                }

            }

        },

        /**
         * This will be called on a mouse hover end
         * Sets cursor style to 'default', hide the element and scale down the infospot
         * @memberOf Infospot
         * @instance
         */
        onHoverEnd: function () {

            if ( !this.getContainer() ) { return; }

            const { scaleDownAnimation, scaleUpAnimation, element } = this;

            this.isHovering = false;
            this.container.style.cursor = 'default';

            if ( this.animated ) {

                scaleUpAnimation.stop();
                scaleDownAnimation.start();

            }

            if ( element && !this.element.locked ) {

                const { left, right, style } = element;

                style.display = 'none';
                if ( left ) { left.style.display = 'none'; }
                if ( right ) { right.style.display = 'none'; }

                this.unlockHoverElement();

            }

        },

        /**
         * On dual eye effect handler
         * Creates duplicate left and right element
         * @param  {object} event - panolens-dual-eye-effect event
         * @memberOf Infospot
         * @instance
         */
        onDualEyeEffect: function ( event ) {

            if ( !this.getContainer() ) { return; }

            let element, halfWidth, halfHeight;

            this.mode = event.mode;

            element = this.element;

            halfWidth = this.container.clientWidth / 2;
            halfHeight = this.container.clientHeight / 2;

            if ( !element ) {

                return;

            }

            if ( !element.left && !element.right ) {

                element.left = element.cloneNode( true );
                element.right = element.cloneNode( true );

            }

            if ( this.mode === MODES.CARDBOARD || this.mode === MODES.STEREO ) {

                element.left.style.display = element.style.display;
                element.right.style.display = element.style.display;
                element.style.display = 'none';

            } else {

                element.style.display = element.left.style.display;
                element.left.style.display = 'none';
                element.right.style.display = 'none';

            }

            // Update elements translation
            this.translateElement( halfWidth, halfHeight );

            this.container.appendChild( element.left );
            this.container.appendChild( element.right );

        },

        /**
         * Translate the hovering element by css transform
         * @param  {number} x - X position on the window screen
         * @param  {number} y - Y position on the window screen
         * @memberOf Infospot
         * @instance
         */
        translateElement: function ( x, y ) {

            if ( !this.element._width || !this.element._height || !this.getContainer() ) {

                return;

            }

            let left, top, element, width, height, delta, container;

            container = this.container;
            element = this.element;
            width = element._width / 2;
            height = element._height / 2;
            delta = element.verticalDelta !== undefined ? element.verticalDelta : 40;

            left = x - width;
            top = y - height - delta;

            if ( ( this.mode === MODES.CARDBOARD || this.mode === MODES.STEREO )
    				&& element.left && element.right
    				&& !( x === container.clientWidth / 2 && y === container.clientHeight / 2 ) ) {

                left = container.clientWidth / 4 - width + ( x - container.clientWidth / 2 );
                top = container.clientHeight / 2 - height - delta + ( y - container.clientHeight / 2 );

                this.setElementStyle( 'transform', element.left, 'translate(' + left + 'px, ' + top + 'px)' );

                left += container.clientWidth / 2;

                this.setElementStyle( 'transform', element.right, 'translate(' + left + 'px, ' + top + 'px)' );

            } else {

                this.setElementStyle( 'transform', element, 'translate(' + left + 'px, ' + top + 'px)' );

            }

        },

        /**
         * Set vendor specific css
         * @param {string} type - CSS style name
         * @param {HTMLElement} element - The element to be modified
         * @param {string} value - Style value
         * @memberOf Infospot
         * @instance
         */
        setElementStyle: function ( type, element, value ) {

            const style = element.style;

            if ( type === 'transform' ) {

                style.webkitTransform = style.msTransform = style.transform = value;

            }

        },

        /**
         * Set hovering text content
         * @param {string} text - Text to be displayed
         * @memberOf Infospot
         * @instance
         */
        setText: function ( text ) {

            if ( this.element ) {

                this.element.textContent = text;

            }

        },

        /**
         * Set cursor css style on hover
         * @memberOf Infospot
         * @instance
         */
        setCursorHoverStyle: function ( style ) {

            this.cursorStyle = style;

        },

        /**
         * Add hovering text element
         * @param {string} text - Text to be displayed
         * @param {number} [delta=40] - Vertical delta to the infospot
         * @memberOf Infospot
         * @instance
         */
        addHoverText: function ( text, delta = 40 ) {

            if ( !this.element ) {

                this.element = document.createElement( 'div' );
                this.element.style.display = 'none';
                this.element.style.color = '#fff';
                this.element.style.top = 0;
                this.element.style.maxWidth = '50%';
                this.element.style.maxHeight = '50%';
                this.element.style.textShadow = '0 0 3px #000000';
                this.element.style.fontFamily = '"Trebuchet MS", Helvetica, sans-serif';
                this.element.style.position = 'absolute';
                this.element.classList.add( 'panolens-infospot' );
                this.element.verticalDelta = delta;

            }

            this.setText( text );

        },

        /**
         * Add hovering element by cloning an element
         * @param {HTMLDOMElement} el - Element to be cloned and displayed
         * @param {number} [delta=40] - Vertical delta to the infospot
         * @memberOf Infospot
         * @instance
         */
        addHoverElement: function ( el, delta = 40 ) {

            if ( !this.element ) {

                this.element = el.cloneNode( true );
                this.element.style.display = 'none';
                this.element.style.top = 0;
                this.element.style.position = 'absolute';
                this.element.classList.add( 'panolens-infospot' );
                this.element.verticalDelta = delta;

            }

        },

        /**
         * Remove hovering element
         * @memberOf Infospot
         * @instance
         */
        removeHoverElement: function () {

            if ( this.element ) {

                if ( this.element.left ) {

                    this.container.removeChild( this.element.left );
                    this.element.left = null;

                }

                if ( this.element.right ) {

                    this.container.removeChild( this.element.right );
                    this.element.right = null;

                }

                this.container.removeChild( this.element );
                this.element = null;

            }

        },

        /**
         * Lock hovering element
         * @memberOf Infospot
         * @instance
         */
        lockHoverElement: function () {

            if ( this.element ) {

                this.element.locked = true;

            }

        },

        /**
         * Unlock hovering element
         * @memberOf Infospot
         * @instance
         */
        unlockHoverElement: function () {

            if ( this.element ) {

                this.element.locked = false;

            }

        },

        /**
         * Enable raycasting
         * @param {boolean} [enabled=true]
         * @memberOf Infospot
         * @instance
         */
        enableRaycast: function ( enabled = true ) {

            if ( enabled ) {

                this.raycast = this.originalRaycast;

            } else {

                this.raycast = () => {};

            }

        },

        /**
         * Show infospot
         * @param  {number} [delay=0] - Delay time to show
         * @memberOf Infospot
         * @instance
         */
        show: function ( delay = 0 ) {

            const { animated, hideAnimation, showAnimation, material } = this;

            if ( animated ) {

                hideAnimation.stop();
                showAnimation.delay( delay ).start();

            } else {

                this.enableRaycast( true );
                material.opacity = 1;

            }

        },

        /**
         * Hide infospot
         * @param  {number} [delay=0] - Delay time to hide
         * @memberOf Infospot
         * @instance
         */
        hide: function ( delay = 0 ) {

            const { animated, hideAnimation, showAnimation, material } = this;

            if ( animated ) {

                showAnimation.stop();
                hideAnimation.delay( delay ).start();

            } else {

                this.enableRaycast( false );
                material.opacity = 0;

            }

        },

        /**
         * Set focus event handler
         * @memberOf Infospot
         * @instance
         */
        setFocusMethod: function ( event ) {

            if ( event ) {

                this.HANDLER_FOCUS = event.method;

            }

        },

        /**
         * Focus camera center to this infospot
         * @param {number} [duration=1000] - Duration to tween
         * @param {function} [easing=TWEEN.Easing.Exponential.Out] - Easing function
         * @memberOf Infospot
         * @instance
         */
        focus: function ( duration, easing ) {

            if ( this.HANDLER_FOCUS ) {

                this.HANDLER_FOCUS( this.position, duration, easing );
                this.onDismiss();

            }

        },

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

            const { geometry, material } = this;
            const { map } = material;

            this.removeHoverElement();

            if ( this.parent ) {

                this.parent.remove( this );

            }

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

        }

    } );

    export { Infospot };