# panorama/LittlePlanet.js

    import { ImagePanorama } from './ImagePanorama';
    import { Infospot } from '../infospot/Infospot';
    import { CONTROLS } from '../Constants';
    import { StereographicShader } from '../shaders/StereographicShader';
    import * as THREE from 'three';

    /**
     * @classdesc Little Planet
     * @constructor
     * @param {string} type 		- Type of little planet basic class
     * @param {string} source 		- URL for the image source
     * @param {number} [size=10000] - Size of plane geometry
     * @param {number} [ratio=0.5]  - Ratio of plane geometry's height against width
     */
    function LittlePlanet ( type = 'image', source, size = 10000, ratio = 0.5 ) {

        if ( type === 'image' ) {

            ImagePanorama.call( this, source, this.createGeometry( size, ratio ), this.createMaterial( size ) );

        }

        this.size = size;
        this.ratio = ratio;
        this.EPS = 0.000001;
        this.frameId = null;

        this.dragging = false;
        this.userMouse = new THREE.Vector2();

        this.quatA = new THREE.Quaternion();
        this.quatB = new THREE.Quaternion();
        this.quatCur = new THREE.Quaternion();
        this.quatSlerp = new THREE.Quaternion();

        this.vectorX = new THREE.Vector3( 1, 0, 0 );
        this.vectorY = new THREE.Vector3( 0, 1, 0 );

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

    }

    LittlePlanet.prototype = Object.assign( Object.create( ImagePanorama.prototype ), {

        constructor: LittlePlanet,

        add: function ( object ) {

            if ( arguments.length > 1 ) {

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

                    this.add( arguments[ i ] );

                }

                return this;

            }

            if ( object instanceof Infospot ) {

                object.material.depthTest = false;

            }

            ImagePanorama.prototype.add.call( this, object );

        },

        createGeometry: function ( size, ratio ) {

            return new THREE.PlaneBufferGeometry( size, size * ratio );

        },

        createMaterial: function ( size ) {

            const shader = Object.assign( {}, StereographicShader ), uniforms = shader.uniforms;

            uniforms.zoom.value = size;
            uniforms.opacity.value = 0.0;

            return new THREE.ShaderMaterial( {

                uniforms: uniforms,
                vertexShader: shader.vertexShader,
                fragmentShader: shader.fragmentShader,
                side: THREE.BackSide,
                transparent: true

            } );

        },

        registerMouseEvents: function () {

            this.container.addEventListener( 'mousedown', this.onMouseDown.bind( this ), { passive: true } );
            this.container.addEventListener( 'mousemove', this.onMouseMove.bind( this ), { passive: true } );
            this.container.addEventListener( 'mouseup', this.onMouseUp.bind( this ), { passive: true } );
            this.container.addEventListener( 'touchstart', this.onMouseDown.bind( this ), { passive: true } );
            this.container.addEventListener( 'touchmove', this.onMouseMove.bind( this ), { passive: true } );
            this.container.addEventListener( 'touchend', this.onMouseUp.bind( this ), { passive: true } );
            this.container.addEventListener( 'mousewheel', this.onMouseWheel.bind( this ), { passive: false } );
            this.container.addEventListener( 'DOMMouseScroll', this.onMouseWheel.bind( this ), { passive: false } );
            this.container.addEventListener( 'contextmenu', this.onContextMenu.bind( this ), { passive: true } );

        },

        unregisterMouseEvents: function () {

            this.container.removeEventListener( 'mousedown', this.onMouseDown.bind( this ), false );
            this.container.removeEventListener( 'mousemove', this.onMouseMove.bind( this ), false );
            this.container.removeEventListener( 'mouseup', this.onMouseUp.bind( this ), false );
            this.container.removeEventListener( 'touchstart', this.onMouseDown.bind( this ), false );
            this.container.removeEventListener( 'touchmove', this.onMouseMove.bind( this ), false );
            this.container.removeEventListener( 'touchend', this.onMouseUp.bind( this ), false );
            this.container.removeEventListener( 'mousewheel', this.onMouseWheel.bind( this ), false );
            this.container.removeEventListener( 'DOMMouseScroll', this.onMouseWheel.bind( this ), false );
            this.container.removeEventListener( 'contextmenu', this.onContextMenu.bind( this ), false );

        },

        onMouseDown: function ( event ) {

            const inputCount = ( event.touches && event.touches.length ) || 1 ;

            switch ( inputCount ) {

            case 1:

                const x = ( event.clientX >= 0 ) ? event.clientX : event.touches[ 0 ].clientX;
                const y = ( event.clientY >= 0 ) ? event.clientY : event.touches[ 0 ].clientY;

                this.dragging = true;
                this.userMouse.set( x, y );

                break;

            case 2:

                const dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
                const dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
                const distance = Math.sqrt( dx * dx + dy * dy );
                this.userMouse.pinchDistance = distance;

                break;

            default:

                break;

            }

            this.onUpdateCallback();

        },

        onMouseMove: function ( event ) {

            const inputCount = ( event.touches && event.touches.length ) || 1 ;

            switch ( inputCount ) {

            case 1:

                const x = ( event.clientX >= 0 ) ? event.clientX : event.touches[ 0 ].clientX;
                const y = ( event.clientY >= 0 ) ? event.clientY : event.touches[ 0 ].clientY;

                const angleX = THREE.Math.degToRad( x - this.userMouse.x ) * 0.4;
                const angleY = THREE.Math.degToRad( y - this.userMouse.y ) * 0.4;

                if ( this.dragging ) {
                    this.quatA.setFromAxisAngle( this.vectorY, angleX );
                    this.quatB.setFromAxisAngle( this.vectorX, angleY );
                    this.quatCur.multiply( this.quatA ).multiply( this.quatB );
                    this.userMouse.set( x, y );
                }

                break;

            case 2:

                const dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
                const dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
                const distance = Math.sqrt( dx * dx + dy * dy );

                this.addZoomDelta( this.userMouse.pinchDistance - distance );

                break;

            default:

                break;

            }

        },

        onMouseUp: function () {

            this.dragging = false;

        },

        onMouseWheel: function ( event ) {

            event.preventDefault();
            event.stopPropagation();

            let delta = 0;

            if ( event.wheelDelta !== undefined ) { // WebKit / Opera / Explorer 9

                delta = event.wheelDelta;

            } else if ( event.detail !== undefined ) { // Firefox

                delta = - event.detail;

            }

            this.addZoomDelta( delta );
            this.onUpdateCallback();

        },

        addZoomDelta: function ( delta ) {

            const uniforms = this.material.uniforms;
            const lowerBound = this.size * 0.1;
            const upperBound = this.size * 10;

            uniforms.zoom.value += delta;

            if ( uniforms.zoom.value <= lowerBound ) {

                uniforms.zoom.value = lowerBound;

            } else if ( uniforms.zoom.value >= upperBound ) {

                uniforms.zoom.value = upperBound;

            }

        },

        onUpdateCallback: function () {

            this.frameId = window.requestAnimationFrame( this.onUpdateCallback.bind( this ) );

            this.quatSlerp.slerp( this.quatCur, 0.1 );

            if ( this.material ) {

                this.material.uniforms.transform.value.makeRotationFromQuaternion( this.quatSlerp );

            }

            if ( !this.dragging && 1.0 - this.quatSlerp.clone().dot( this.quatCur ) < this.EPS ) {

                window.cancelAnimationFrame( this.frameId );

            }

        },

        reset: function () {

            this.quatCur.set( 0, 0, 0, 1 );
            this.quatSlerp.set( 0, 0, 0, 1 );
            this.onUpdateCallback();

        },

        onLoad: function ( texture ) {

            this.material.uniforms.resolution.value = this.container.clientWidth / this.container.clientHeight;

            this.registerMouseEvents();
            this.onUpdateCallback();

            this.dispatchEvent( { type: 'panolens-viewer-handler', method: 'disableControl' } );

            ImagePanorama.prototype.onLoad.call( this, texture );

        },

        onLeave: function () {

            this.unregisterMouseEvents();

            this.dispatchEvent( { type: 'panolens-viewer-handler', method: 'enableControl', data: CONTROLS.ORBIT } );

            window.cancelAnimationFrame( this.frameId );

            ImagePanorama.prototype.onLeave.call( this );

        },

        onWindowResize: function () {

            this.material.uniforms.resolution.value = this.container.clientWidth / this.container.clientHeight;

        },

        onContextMenu: function () {

            this.dragging = false;

        },

        dispose: function () {

            this.unregisterMouseEvents();

            ImagePanorama.prototype.dispose.call( this );

        }

    });

    export { LittlePlanet };