import {
	LinearMipmapLinearFilter,
	MeshBasicMaterial,
	NoBlending,
	ShaderMaterial,
	UniformsUtils,
	WebGLRenderTarget
} from 'three';
import { Pass, FullScreenQuad } from './Pass.js';
import { CopyShader } from '../shaders/CopyShader.js';
import { LuminosityShader } from '../shaders/LuminosityShader.js';
import { ToneMapShader } from '../shaders/ToneMapShader.js';

/**
 * Generate a texture that represents the luminosity of the current scene, adapted over time
 * to simulate the optic nerve responding to the amount of light it is receiving.
 * Based on a GDC2007 presentation by Wolfgang Engel titled "Post-Processing Pipeline"
 *
 * Full-screen tone-mapping shader based on http://www.graphics.cornell.edu/~jaf/publications/sig02_paper.pdf
 */

class AdaptiveToneMappingPass extends Pass {

	constructor( adaptive, resolution ) {

		super();

		this.resolution = ( resolution !== undefined ) ? resolution : 256;
		this.needsInit = true;
		this.adaptive = adaptive !== undefined ? !! adaptive : true;

		this.luminanceRT = null;
		this.previousLuminanceRT = null;
		this.currentLuminanceRT = null;

		const copyShader = CopyShader;

		this.copyUniforms = UniformsUtils.clone( copyShader.uniforms );

		this.materialCopy = new ShaderMaterial( {

			uniforms: this.copyUniforms,
			vertexShader: copyShader.vertexShader,
			fragmentShader: copyShader.fragmentShader,
			blending: NoBlending,
			depthTest: false

		} );

		this.materialLuminance = new ShaderMaterial( {

			uniforms: UniformsUtils.clone( LuminosityShader.uniforms ),
			vertexShader: LuminosityShader.vertexShader,
			fragmentShader: LuminosityShader.fragmentShader,
			blending: NoBlending
		} );

		this.adaptLuminanceShader = {
			defines: {
				'MIP_LEVEL_1X1': ( Math.log( this.resolution ) / Math.log( 2.0 ) ).toFixed( 1 )
			},
			uniforms: {
				'lastLum': { value: null },
				'currentLum': { value: null },
				'minLuminance': { value: 0.01 },
				'delta': { value: 0.016 },
				'tau': { value: 1.0 }
			},
			vertexShader:
				`varying vec2 vUv;

				void main() {

					vUv = uv;
					gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );

				}`,

			fragmentShader:
				`varying vec2 vUv;

				uniform sampler2D lastLum;
				uniform sampler2D currentLum;
				uniform float minLuminance;
				uniform float delta;
				uniform float tau;

				void main() {

					vec4 lastLum = texture2D( lastLum, vUv, MIP_LEVEL_1X1 );
					vec4 currentLum = texture2D( currentLum, vUv, MIP_LEVEL_1X1 );

					float fLastLum = max( minLuminance, lastLum.r );
					float fCurrentLum = max( minLuminance, currentLum.r );

					//The adaption seems to work better in extreme lighting differences
					//if the input luminance is squared.
					fCurrentLum *= fCurrentLum;

					// Adapt the luminance using Pattanaik's technique
					float fAdaptedLum = fLastLum + (fCurrentLum - fLastLum) * (1.0 - exp(-delta * tau));
					// "fAdaptedLum = sqrt(fAdaptedLum);
					gl_FragColor.r = fAdaptedLum;
				}`

		};

		this.materialAdaptiveLum = new ShaderMaterial( {

			uniforms: UniformsUtils.clone( this.adaptLuminanceShader.uniforms ),
			vertexShader: this.adaptLuminanceShader.vertexShader,
			fragmentShader: this.adaptLuminanceShader.fragmentShader,
			defines: Object.assign( {}, this.adaptLuminanceShader.defines ),
			blending: NoBlending
		} );

		this.materialToneMap = new ShaderMaterial( {

			uniforms: UniformsUtils.clone( ToneMapShader.uniforms ),
			vertexShader: ToneMapShader.vertexShader,
			fragmentShader: ToneMapShader.fragmentShader,
			blending: NoBlending
		} );

		this.fsQuad = new FullScreenQuad( null );

	}

	render( renderer, writeBuffer, readBuffer, deltaTime/*, maskActive*/ ) {

		if ( this.needsInit ) {

			this.reset( renderer );

			this.luminanceRT.texture.type = readBuffer.texture.type;
			this.previousLuminanceRT.texture.type = readBuffer.texture.type;
			this.currentLuminanceRT.texture.type = readBuffer.texture.type;
			this.needsInit = false;

		}

		if ( this.adaptive ) {

			//Render the luminance of the current scene into a render target with mipmapping enabled
			this.fsQuad.material = this.materialLuminance;
			this.materialLuminance.uniforms.tDiffuse.value = readBuffer.texture;
			renderer.setRenderTarget( this.currentLuminanceRT );
			this.fsQuad.render( renderer );

			//Use the new luminance values, the previous luminance and the frame delta to
			//adapt the luminance over time.
			this.fsQuad.material = this.materialAdaptiveLum;
			this.materialAdaptiveLum.uniforms.delta.value = deltaTime;
			this.materialAdaptiveLum.uniforms.lastLum.value = this.previousLuminanceRT.texture;
			this.materialAdaptiveLum.uniforms.currentLum.value = this.currentLuminanceRT.texture;
			renderer.setRenderTarget( this.luminanceRT );
			this.fsQuad.render( renderer );

			//Copy the new adapted luminance value so that it can be used by the next frame.
			this.fsQuad.material = this.materialCopy;
			this.copyUniforms.tDiffuse.value = this.luminanceRT.texture;
			renderer.setRenderTarget( this.previousLuminanceRT );
			this.fsQuad.render( renderer );

		}

		this.fsQuad.material = this.materialToneMap;
		this.materialToneMap.uniforms.tDiffuse.value = readBuffer.texture;

		if ( this.renderToScreen ) {

			renderer.setRenderTarget( null );
			this.fsQuad.render( renderer );

		} else {

			renderer.setRenderTarget( writeBuffer );

			if ( this.clear ) renderer.clear();

			this.fsQuad.render( renderer );

		}

	}

	reset() {

		// render targets
		if ( this.luminanceRT ) {

			this.luminanceRT.dispose();

		}

		if ( this.currentLuminanceRT ) {

			this.currentLuminanceRT.dispose();

		}

		if ( this.previousLuminanceRT ) {

			this.previousLuminanceRT.dispose();

		}


		this.luminanceRT = new WebGLRenderTarget( this.resolution, this.resolution );
		this.luminanceRT.texture.name = 'AdaptiveToneMappingPass.l';
		this.luminanceRT.texture.generateMipmaps = false;

		this.previousLuminanceRT = new WebGLRenderTarget( this.resolution, this.resolution );
		this.previousLuminanceRT.texture.name = 'AdaptiveToneMappingPass.pl';
		this.previousLuminanceRT.texture.generateMipmaps = false;

		// We only need mipmapping for the current luminosity because we want a down-sampled version to sample in our adaptive shader

		const pars = { minFilter: LinearMipmapLinearFilter, generateMipmaps: true };

		this.currentLuminanceRT = new WebGLRenderTarget( this.resolution, this.resolution, pars );
		this.currentLuminanceRT.texture.name = 'AdaptiveToneMappingPass.cl';

		if ( this.adaptive ) {

			this.materialToneMap.defines[ 'ADAPTED_LUMINANCE' ] = '';
			this.materialToneMap.uniforms.luminanceMap.value = this.luminanceRT.texture;

		}

		//Put something in the adaptive luminance texture so that the scene can render initially
		this.fsQuad.material = new MeshBasicMaterial( { color: 0x777777 } );
		this.materialLuminance.needsUpdate = true;
		this.materialAdaptiveLum.needsUpdate = true;
		this.materialToneMap.needsUpdate = true;
		// renderer.render( this.scene, this.camera, this.luminanceRT );
		// renderer.render( this.scene, this.camera, this.previousLuminanceRT );
		// renderer.render( this.scene, this.camera, this.currentLuminanceRT );

	}

	setAdaptive( adaptive ) {

		if ( adaptive ) {

			this.adaptive = true;
			this.materialToneMap.defines[ 'ADAPTED_LUMINANCE' ] = '';
			this.materialToneMap.uniforms.luminanceMap.value = this.luminanceRT.texture;

		} else {

			this.adaptive = false;
			delete this.materialToneMap.defines[ 'ADAPTED_LUMINANCE' ];
			this.materialToneMap.uniforms.luminanceMap.value = null;

		}

		this.materialToneMap.needsUpdate = true;

	}

	setAdaptionRate( rate ) {

		if ( rate ) {

			this.materialAdaptiveLum.uniforms.tau.value = Math.abs( rate );

		}

	}

	setMinLuminance( minLum ) {

		if ( minLum ) {

			this.materialToneMap.uniforms.minLuminance.value = minLum;
			this.materialAdaptiveLum.uniforms.minLuminance.value = minLum;

		}

	}

	setMaxLuminance( maxLum ) {

		if ( maxLum ) {

			this.materialToneMap.uniforms.maxLuminance.value = maxLum;

		}

	}

	setAverageLuminance( avgLum ) {

		if ( avgLum ) {

			this.materialToneMap.uniforms.averageLuminance.value = avgLum;

		}

	}

	setMiddleGrey( middleGrey ) {

		if ( middleGrey ) {

			this.materialToneMap.uniforms.middleGrey.value = middleGrey;

		}

	}

	dispose() {

		if ( this.luminanceRT ) {

			this.luminanceRT.dispose();

		}

		if ( this.previousLuminanceRT ) {

			this.previousLuminanceRT.dispose();

		}

		if ( this.currentLuminanceRT ) {

			this.currentLuminanceRT.dispose();

		}

		if ( this.materialLuminance ) {

			this.materialLuminance.dispose();

		}

		if ( this.materialAdaptiveLum ) {

			this.materialAdaptiveLum.dispose();

		}

		if ( this.materialCopy ) {

			this.materialCopy.dispose();

		}

		if ( this.materialToneMap ) {

			this.materialToneMap.dispose();

		}

	}

}

export { AdaptiveToneMappingPass };
