# FluxRenderer.js

    'use strict';

    import THREE from 'three';
    import EdgesHelper from './EdgesHelper.js';
    import FluxCameras from './FluxCameras.js';
    import FluxHelpers from './helpers/FluxHelpers.js';
    import FluxRenderContext from './FluxRenderContext.js';
    import * as FluxThreePlugins from 'flux-three-plugins/src/index.js';
    import * as shaders from './shaders.js';

    /**
     * Class wrapping the renderer with custom passes and context swapping.
     * Multipass rendering uses GPU shaders to accomplish ambient obscurance
     * and stencil buffer shadows.
     * Context swapping lets a single OpenGL context and canvas be used for multiple renderers.
     * @class FluxRenderer
     * @param {Element} domParent The div container for the canvas
     * @param {Number} width     The width of the canvas
     * @param {Number} height    The height of the canvas
     */
    export default function FluxRenderer(domParent, width, height) {
        this.id = FluxRenderer.nextId++;

        // Dom element that wraps the canvas
        this._domParent = domParent;

        // Determines if multipass rendering (FluxThreePlugins.EffectsComposer) is used
        this._multipass = false;

        // TODO: Convert this to a list of passes rather than individual bools
        // Determines if ambient occlusion is used (requires multipass to be true)
        this._showOcclusion = true;

        // Determines if stencilbuffer shadows are used (requires multipass)
        this._showShadows = false;

        // Current three.js geometry to render
        this._model = null;

        // The object containing the lights in the scene
        this._lights = null;

        // The context that contains the renderer and corresponds to a canvas
        // Create renderer for the first time.
        this._context = FluxRenderContext.getNewContext();

        // EffectComposer object, used in multipass rendering
        this._composer = new FluxThreePlugins.EffectComposer(this._context.renderer);

        this._width = width;
        this._height = height;

        this._createCacheCanvas(width, height);

        this.setClearColor(0xC5CDCC);

        this._cameras = new FluxCameras(width, height);
        this._helpers = new FluxHelpers();
        this._helpersScene = new THREE.Scene();
        this._helpersScene.add(this._helpers);

        // Camera to be rendered with.Any instance of `THREE.Camera` can be set here.
        this._initControls();

        // Scene containing geometry to be rendered in this viewport THREE.Scene
        this._scene = new THREE.Scene();
        // this._scene.add(this._helpers);

        // Fog object for this viewport constructed from color and density
        this._fog = new THREE.FogExp2( this._clearColor, 0.0 );
        this._scene.fog = this._fog;

        // Scene containing edges geometry for hidden line rendering
        this._edgesScene = new THREE.Scene();
        this._edgesMode = EdgesHelper.EDGES_MODES.NONE;

        // variables for stencilbuffer shadows
        // Scene holding shadow volumes = THREE.Scene
        this._shadowScene = new THREE.Scene();
        // Color of shadows (multiplied with ground) @type {THREE.Color}
        this._shadowColor = new THREE.Color(0.08, 0.0, 0.2);
        // Alpha opacity of shadow, where 1.0 is completely opaque
        this._shadowAlpha = 0.35;
        this._shadowBuilder = new FluxThreePlugins.ShadowBuilder(this._shadowLight);

        this._addPasses();
    }

    FluxRenderer.prototype = Object.create( THREE.EventDispatcher.prototype );
    FluxRenderer.prototype.constructor = FluxRenderer;

    // Used for debugging issues with _setHost
    FluxRenderer.nextId = 0;

    /**
     * Name of the event fired when the camera changes
     * @type {String}
     */
    FluxRenderer.CHANGE_EVENT = 'change';

    /**
     * Set the lights used to illuminate the scene.
     * @param {THREE.Object3D} lights Object with lights as children
     */
    FluxRenderer.prototype.setLights = function(lights) {
        if (this._lights) {
            this._scene.remove(this._lights);
        }
        this._lights = lights;
        this._scene.add(this._lights);
    };

    /**
     * Remove the geometry objects from the THREE registry so they can be garbage collected
     * @param  {THREE.Object3D} obj The object being removed
     */
    function _removeGeometries(obj) {
        if (obj.geometry) {
            obj.geometry.dispose();
        }
    }

    /**
     * Remove an object from the scene and clean up memory
     * @param  {THREE.Scene} scene Scene containing the model
     * @param  {THREE.Object3D} model The geometry to remove
     */
    function _deleteFromScene(scene, model) {
        if (!model || !scene) return;
        scene.remove(model);
        model.traverse(_removeGeometries);
    }

    /**
     * Set the object to render
     * Replaces old render contents
     * @param {THREE.Object3D} model What to render
     */
    FluxRenderer.prototype.setModel = function(model) {
        if (this._model) {
            _deleteFromScene(this._scene, this._model);
            _deleteFromScene(this._edgesScene, this._model.edgesHelper);
        }
        this._model = model;
        if (this._model) {
            this._scene.add(this._model);
            if (EdgesHelper.AddEdges(this._model, this._edgesMode)) {
                this._edgesScene.add(this._model.edgesHelper);
            }
        }
    };

    /**
     * Set the edges rendering mode for hidden line rendering
     * @param  {EdgesHelper.EDGES_MODES} mode Whether to render front, back, both or none
     */
    FluxRenderer.prototype.setEdgesMode = function(mode) {
        this._edgesMode = mode;
    };

    /**
     * When the camera controls change make sure to update the camera
     * @private
     */
    FluxRenderer.prototype._initControls = function() {
        if (this._controls) this._controls.enabled = false;
        //TODO(Kyle): rewrite EditorControls to allow camera to be changed
        this._controls = new FluxThreePlugins.EditorControls(this._cameras.getCamera(), this._domParent);
        var _this = this;
        this._controls.addEventListener(FluxRenderer.CHANGE_EVENT, function(event) {
            _this._cameras.updateCamera(_this._width, _this._height);
            _this.dispatchEvent( event );
        });
    };

    /**
     * Restore the camera to a default location
     */
    FluxRenderer.prototype.homeCamera = function() {
        this._controls.focus(this._helpers, true);
    };

    /**
    * Focus the controls' current camera on an object.
    * This function will focus on the union of object and all of it's visible children.
    * @param  {THREE.Object3D} object The scene object to focus on.
    */
    FluxRenderer.prototype.focus = function() {
        if (!this._model) return;
        this._controls.focus(this._model, true);
        // Changing the controls here triggers a render
    };

    /**
     * Set the clear color (background) for WebGL canvas
     * @param {String|Number} color Hexadecimal or a CSS-style string
     */
    FluxRenderer.prototype.setClearColor = function(color) {
        this._clearColor = new THREE.Color(color);
    };

    /**
     * Set up a new canvas used for storing a cached image.
     * The cache image is populated when this renderer loses it's context.
     * @private
     * @param {Number} width The width of the canvas
     * @param {Number} height The height of the canvas
     */
    FluxRenderer.prototype._createCacheCanvas = function(width, height) {
        if (this._cacheCanvas) return;
        // The canvas used to store a cached image of the previous render when all the WebGL contexts are in use with other renderers
        this._cacheCanvas = document.createElement('canvas');
        this._cacheCanvas.width = width;
        this._cacheCanvas.height = height;
        this._cacheCanvas.style.position = 'absolute';
        this._cacheCanvas.style['user-select'] = 'none';
        this._cacheCanvas.style['-webkit-user-select'] = 'none';
        this._domParent.appendChild(this._cacheCanvas);

        // Canvas2D used to store framebuffer pixels after renderer.domElement migration.
        this.ctx = this._cacheCanvas.getContext('2d');
    };

    /**
     * Destructor to prevent future rendering after being unloaded
     */
    FluxRenderer.prototype.detach = function() {
        if (this._context && this._context.currentHost === this) {
            this._context.currentHost = null;
        }
    };

    /**
     * Change the camera view
     * @param {String} view The new view mode
     */
    FluxRenderer.prototype.setView = function(view) {
        this._cameras.setView(view);
        this._cameras.updateCamera(this._width, this._height);
        this._initControls();
        this._helpers.setView(view);

        if (!this._renderPasses) return;
        this._renderPasses.renderPass.camera = this._cameras.getCamera();
        this._renderPasses.edgesPass.camera = this._cameras.getCamera();
        this._renderPasses.helperPass.camera = this._cameras.getCamera();
        this._renderPasses.stencilPass.camera = this._cameras.getCamera();
    };

    /**
     * Creates depth, normal materials and depth, normal render targets.
     * @private
     */
    FluxRenderer.prototype._addRenderTargets = function() {
        // depth render target (uses THREE.js depth shader)
        this._depthTarget = new THREE.WebGLRenderTarget(
            window.innerWidth, //TODO(kyle) Why does this use window!?
            window.innerHeight,
            {
                minFilter: THREE.NearestFilter,
                magFilter: THREE.NearestFilter,
                format: THREE.RGBAFormat
            }
        );

        // normal render target
        this._normalTarget = this._depthTarget.clone();
    };

    /**
     * Adds enabled passes to the EffectComposer
     *
     * Always begins with a render pass
     * Always ends with an antialiasing (FXAA) pass
     *
     * May include the following: Ambient occlusion, Shadows
     * @private
     */
    FluxRenderer.prototype._addPasses = function() {
        // _renderPasses dictionary for holding passes that need to be accessed or modified
        this._renderPasses = {};
        this._addRenderTargets();

        // diffuse render pass
        var renderPass = new FluxThreePlugins.RenderPass(this._scene, this._cameras.getCamera());
        this._composer.addPass(renderPass);
        this._renderPasses.renderPass = renderPass;

        var edgesPass = new FluxThreePlugins.RenderPass(this._edgesScene, this._cameras.getCamera());
        edgesPass.clear = false;
        this._composer.addPass(edgesPass);
        edgesPass.polygonOffset = true;
        edgesPass.enabled = !!this._edgesScene;
        this._renderPasses.edgesPass = edgesPass;

        // helper render pass
        var helperPass = new FluxThreePlugins.RenderPass(this._helpersScene, this._cameras.getCamera());
        helperPass.clear = false;
        this._composer.addPass(helperPass);
        this._renderPasses.helperPass = helperPass;
        helperPass.enabled = true;

        // ambient occlusion pass
        var aoPass = new FluxThreePlugins.ShaderPass(FluxThreePlugins.SAOShader);
        // set uniform vars for ao pass
        aoPass.uniforms.tDepth.value = this._depthTarget;
        aoPass.uniforms.tNorm.value = this._normalTarget;
        aoPass.uniforms.projInv.value = new THREE.Matrix4();//TODO.getInverse(this._cameras.getCamera().projectionMatrix);
        aoPass.uniforms.onlyAO.value = false; // set to true to view only ambient occlusion
        aoPass.clear = true;
        aoPass.needsSwap = true;
        this._renderPasses.aoPass = aoPass;
        this._composer.addPass(aoPass);

        // stencil buffer shadow passes
        var copyPass = new FluxThreePlugins.ShaderPass( FluxThreePlugins.CopyShader );
        copyPass.needsSwap = false;
        this._composer.addPass( copyPass ); // copy AO to write buffer
        this._renderPasses.copyPass = copyPass;

        var stencilPass = new FluxThreePlugins.StencilPass( this._shadowScene, this._cameras.getCamera());
        this._composer.addPass( stencilPass ); // render shadow volumes to stencilbuffer
        this._renderPasses.stencilPass = stencilPass;

        var darkenPass = new FluxThreePlugins.ShaderPass( FluxThreePlugins.DarkenShader );
        darkenPass.uniforms.alpha.value = this._shadowAlpha;
        darkenPass.uniforms.color.value = new THREE.Vector3(this._shadowColor.r, this._shadowColor.g, this._shadowColor.b);
        darkenPass.needsSwap = false;
        this._composer.addPass( darkenPass ); // darken stencil areas
        this._renderPasses.darkenPass = darkenPass;

        var clearPass = new FluxThreePlugins.ClearStencilPass();
        clearPass.needsSwap = true;
        this._composer.addPass( clearPass ); // clear stencil
        this._renderPasses.clearPass = clearPass;

        // fast approximate antialiasing pass
        var FXAAPass = new FluxThreePlugins.ShaderPass(FluxThreePlugins.FXAAShader);
        FXAAPass.renderToScreen = true;
        this._renderPasses.FXAAPass = FXAAPass;
        this._composer.addPass(FXAAPass);
    };

    /**
     * For multipass rendering, update which render passes are enabled.
     * Based on user preferences some passes may be turned on or off.
     * Also passes may be disabled if the corresponding scene is empty.
     * @private
     */
    FluxRenderer.prototype._updatePasses = function () {
        if (this._showOcclusion) {
            // populate depth target
            this._scene.overrideMaterial = shaders.DEPTH_MATERIAL;
            this._context.renderer.clearTarget( this._depthTarget, true, true );
            this._context.renderer.render( this._scene, this._cameras.getCamera(), this._depthTarget );

            // populate normal target (set clearColor to (0,0,0) since
            // empty pixels do not have normals)
            this._context.renderer.setClearColor( 0x000000 );
            this._scene.overrideMaterial = shaders.NORMAL_MATERIAL;
            this._context.renderer.clearTarget( this._normalTarget, true, true );
            this._context.renderer.render( this._scene, this._cameras.getCamera(), this._normalTarget );
            this._scene.overrideMaterial = null;
            this._context.renderer.setClearColor( this._clearColor );

            // update ambient occlusion shader uniforms
            var projInv = new THREE.Matrix4();
            projInv.getInverse(this._cameras.getCamera().projectionMatrix);
            this._renderPasses.aoPass.uniforms.projInv.value = projInv;
            this._renderPasses.aoPass.uniforms.size.value.set(this._width, this._height);
            this._renderPasses.aoPass.uniforms.onlyDiffuse.value = false;
        } else {
            this._renderPasses.aoPass.uniforms.onlyDiffuse.value = true;
        }

        if (this._showShadows) {
            this._composer.renderTarget1.stencilBuffer = true;
            this._composer.renderTarget2.stencilBuffer = true;
            this._renderPasses.copyPass.enabled = true;
            this._renderPasses.stencilPass.enabled = true;
            this._renderPasses.darkenPass.enabled = true;
            this._renderPasses.clearPass.enabled = true;
        } else {
            this._composer.renderTarget1.stencilBuffer = false;
            this._composer.renderTarget2.stencilBuffer = false;
            this._renderPasses.copyPass.enabled = false;
            this._renderPasses.stencilPass.enabled = false;
            this._renderPasses.darkenPass.enabled = false;
            this._renderPasses.clearPass.enabled = false;
        }

        // set antialiasing 'resolution' uniform to current screen resolution
        this._renderPasses.FXAAPass.uniforms.resolution.value.set(1.0/this._width, 1.0/this._height);
    };

    /**
    * Render the scene with its geometry.
    */
    FluxRenderer.prototype.doRender = function () {
        this._setHost();
        this._update();
        this._context.renderer.clear();
        if (this._multipass) {
            this._updatePasses();
            // render scene
            this._composer.render();
        } else {
            this._context.renderer.render(this._scene, this._cameras.getCamera());
            this._context.renderer.render(this._edgesScene, this._cameras.getCamera());
            this._context.renderer.render(this._helpersScene, this._cameras.getCamera());
        }
    };

    /**
     * Say whether there are any objects to render in the model
     * @return {Boolean} True if there are objects to render
     */
    FluxRenderer.prototype.anyValidPrims = function() {
        return this._model ? this._model.children.length > 0 : false;
    };

    /**
    * Set the light that is casting shadows.
    * @param {THREE.Light} light      light object, position is saved
    */
    FluxRenderer.prototype.setShadowLight = function(light) {
        // Enable shadows on this renderer
        this._multipass = true;
        this._showShadows = true;

        this._shadowLight = light.position;
        this._shadowBuilder.updateLight(light.position);
    };

    /**
     * Add the shadows for everything in the current model
     */
    FluxRenderer.prototype.addShadows = function() {
        var _this = this;
        this._model.traverse(function (obj) {
            if (obj && obj.geometry) {
                _this.addShadow(obj);
            }
        });
    };
    /**
    * Add a shadow to the scene.
    * @param {THREE.Mesh} mesh        The mesh of the object casting a shadow
    */
    FluxRenderer.prototype.addShadow = function(mesh) {
        var shadow = this._shadowBuilder.getShadowVolume(mesh);
        this._shadowScene.add(shadow);
    };

    /**
    * Remove a shadow from the scene.
    * @param  {THREE.Mesh} mesh       The mesh of the shadow to remove
    */
    FluxRenderer.prototype.removeShadow = function(mesh) {
        var shadow = this._shadowBuilder.getShadowVolume(mesh);
        this._shadowScene.remove(shadow);
    };

    /**
     * Copy the image that is in the render canvas to this renderer's cache canvas.
     * This allows the rendered image to persist even when the renderer is not available.
     * This happens when the user moves the mouse away from this viewport to another one.
     * @private
     */
    FluxRenderer.prototype._cacheImageToCanvas = function () {
        this.doRender();
        this.ctx.drawImage(this._context.renderer.domElement, 0, 0, this._cacheCanvas.width, this._cacheCanvas.height);
    };

    /**
     * Get the canvas for use in QA scripts
     * @return {Canvas} WebGL canvas dom element
     */
    FluxRenderer.prototype.getGlCanvas = function() {
        return this._context.renderer.domElement;
    };

    /**
    * Migrate renderer.domElement to this host if necessary
    * and copy framebuffer into Canvas2D element of the previous host.
     * @private
    */
    FluxRenderer.prototype._setHost = function() {
        if (this === this._context.currentHost) return;
        if (this._context.currentHost) {
            // Copy the image from domElement (THREE's interactive canvas)
            // to the 2D context for this element's canvas
            // This image will remain up until the user interacts with the old viewport again
            this._context.currentHost._cacheImageToCanvas();
        }
        this._context.currentHost = this;
        this.setSize(this._width, this._height);
        // Move the THREE.WebGLRenderer's canvas under the new host
        this._domParent.appendChild(this._context.renderer.domElement);
    };

    /**
     * Set the WebGLRenderer parameters to match this renderer.
     * @private
     */
    FluxRenderer.prototype._update = function() {
        this._context.renderer.autoClearColor = this._multipass;
        this._context.renderer.autoClearDepth = this._multipass;
        this._context.renderer.setSize(this._width, this._height);
        this._context.renderer.setClearColor(this._clearColor);
    };

    /**
     * Set the size of things that are per viewport.
     * @param {Number} width  The canvas width in pixels
     * @param {Number} height The canvas height in pixels
     */
    FluxRenderer.prototype.setSize = function(width, height) {
        if (width <= 0 || height <= 0 || (width === this._width && height === this.height)) return;
        this._width = width;
        this._height = height;

        this._cameras.updateCamera(this._width, this._height);

        this._composer.setSize(this._width, this._height);

        this._cacheCanvas.height = height;
        this._cacheCanvas.width = width;
    };

    /**
     * Make serializable by pruning all references and building an object property tree
     * @return {Object} Data to stringify
     */
    FluxRenderer.prototype.toJSON = function() {
        var serializableState = {
            cameras: this._cameras.toJSON(), // camera pos and view
            controls: this._controls.toJSON() // center point
        };
        return serializableState;
    };

    /**
     * Take a data object and use it to update the internal state
     * @param  {Object} state The properties to set
     */
    FluxRenderer.prototype.fromJSON = function(state) {
        if (!state) return;
        if (state.cameras != null) {
            this.setView(state.cameras.view);
            this._cameras.fromJSON(state.cameras);
        }
        if (state.controls) {
            this._controls.fromJSON(state.controls);
        }
    };