// Parallax Occlusion shaders from
//    http://sunandblackcat.com/tipFullView.php?topicid=28
// No tangent-space transforms logic based on
//   http://mmikkelsen3d.blogspot.sk/2012/02/parallaxpoc-mapping-and-no-tangent.html

const ParallaxShader = {
	// Ordered from fastest to best quality.
	modes: {
		none: 'NO_PARALLAX',
		basic: 'USE_BASIC_PARALLAX',
		steep: 'USE_STEEP_PARALLAX',
		occlusion: 'USE_OCLUSION_PARALLAX', // a.k.a. POM
		relief: 'USE_RELIEF_PARALLAX'
	},

	uniforms: {
		'bumpMap': { value: null },
		'map': { value: null },
		'parallaxScale': { value: null },
		'parallaxMinLayers': { value: null },
		'parallaxMaxLayers': { value: null }
	},

	vertexShader: /* glsl */`

		varying vec2 vUv;
		varying vec3 vViewPosition;
		varying vec3 vNormal;

		void main() {

			vUv = uv;
			vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
			vViewPosition = -mvPosition.xyz;
			vNormal = normalize( normalMatrix * normal );
			gl_Position = projectionMatrix * mvPosition;

		}`,

	fragmentShader: /* glsl */`

		uniform sampler2D bumpMap;
		uniform sampler2D map;

		uniform float parallaxScale;
		uniform float parallaxMinLayers;
		uniform float parallaxMaxLayers;

		varying vec2 vUv;
		varying vec3 vViewPosition;
		varying vec3 vNormal;

		#ifdef USE_BASIC_PARALLAX

			vec2 parallaxMap( in vec3 V ) {

				float initialHeight = texture2D( bumpMap, vUv ).r;

				// No Offset Limitting: messy, floating output at grazing angles.
			//"vec2 texCoordOffset = parallaxScale * V.xy / V.z * initialHeight;",

			// Offset Limiting
				vec2 texCoordOffset = parallaxScale * V.xy * initialHeight;
				return vUv - texCoordOffset;

			}

		#else

			vec2 parallaxMap( in vec3 V ) {

				// Determine number of layers from angle between V and N
				float numLayers = mix( parallaxMaxLayers, parallaxMinLayers, abs( dot( vec3( 0.0, 0.0, 1.0 ), V ) ) );

				float layerHeight = 1.0 / numLayers;
				float currentLayerHeight = 0.0;
				// Shift of texture coordinates for each iteration
				vec2 dtex = parallaxScale * V.xy / V.z / numLayers;

				vec2 currentTextureCoords = vUv;

				float heightFromTexture = texture2D( bumpMap, currentTextureCoords ).r;

				// while ( heightFromTexture > currentLayerHeight )
				// Infinite loops are not well supported. Do a "large" finite
				// loop, but not too large, as it slows down some compilers.
				for ( int i = 0; i < 30; i += 1 ) {
					if ( heightFromTexture <= currentLayerHeight ) {
						break;
					}
					currentLayerHeight += layerHeight;
					// Shift texture coordinates along vector V
					currentTextureCoords -= dtex;
					heightFromTexture = texture2D( bumpMap, currentTextureCoords ).r;
				}

				#ifdef USE_STEEP_PARALLAX

					return currentTextureCoords;

				#elif defined( USE_RELIEF_PARALLAX )

					vec2 deltaTexCoord = dtex / 2.0;
					float deltaHeight = layerHeight / 2.0;

					// Return to the mid point of previous layer
					currentTextureCoords += deltaTexCoord;
					currentLayerHeight -= deltaHeight;

					// Binary search to increase precision of Steep Parallax Mapping
					const int numSearches = 5;
					for ( int i = 0; i < numSearches; i += 1 ) {

						deltaTexCoord /= 2.0;
						deltaHeight /= 2.0;
						heightFromTexture = texture2D( bumpMap, currentTextureCoords ).r;
						// Shift along or against vector V
						if( heightFromTexture > currentLayerHeight ) { // Below the surface

							currentTextureCoords -= deltaTexCoord;
							currentLayerHeight += deltaHeight;

						} else { // above the surface

							currentTextureCoords += deltaTexCoord;
							currentLayerHeight -= deltaHeight;

						}

					}
					return currentTextureCoords;

				#elif defined( USE_OCLUSION_PARALLAX )

					vec2 prevTCoords = currentTextureCoords + dtex;

					// Heights for linear interpolation
					float nextH = heightFromTexture - currentLayerHeight;
					float prevH = texture2D( bumpMap, prevTCoords ).r - currentLayerHeight + layerHeight;

					// Proportions for linear interpolation
					float weight = nextH / ( nextH - prevH );

					// Interpolation of texture coordinates
					return prevTCoords * weight + currentTextureCoords * ( 1.0 - weight );

				#else // NO_PARALLAX

					return vUv;

				#endif

			}
		#endif

		vec2 perturbUv( vec3 surfPosition, vec3 surfNormal, vec3 viewPosition ) {

 			vec2 texDx = dFdx( vUv );
			vec2 texDy = dFdy( vUv );

			vec3 vSigmaX = dFdx( surfPosition );
			vec3 vSigmaY = dFdy( surfPosition );
			vec3 vR1 = cross( vSigmaY, surfNormal );
			vec3 vR2 = cross( surfNormal, vSigmaX );
			float fDet = dot( vSigmaX, vR1 );

			vec2 vProjVscr = ( 1.0 / fDet ) * vec2( dot( vR1, viewPosition ), dot( vR2, viewPosition ) );
			vec3 vProjVtex;
			vProjVtex.xy = texDx * vProjVscr.x + texDy * vProjVscr.y;
			vProjVtex.z = dot( surfNormal, viewPosition );

			return parallaxMap( vProjVtex );
		}

		void main() {

			vec2 mapUv = perturbUv( -vViewPosition, normalize( vNormal ), normalize( vViewPosition ) );
			gl_FragColor = texture2D( map, mapUv );

		}`

};

export { ParallaxShader };
