import ShadowNode from './ShadowNode.js';
import { uniform } from '../core/UniformNode.js';
import { float, vec2, If, Fn, nodeObject } from '../tsl/TSLBase.js';
import { reference } from '../accessors/ReferenceNode.js';
import { texture } from '../accessors/TextureNode.js';
import { max, abs, sign } from '../math/MathNode.js';
import { sub, div } from '../math/OperatorNode.js';
import { renderGroup } from '../core/UniformGroupNode.js';
import { Vector2 } from '../../math/Vector2.js';
import { Vector4 } from '../../math/Vector4.js';
import { Color } from '../../math/Color.js';
import { BasicShadowMap } from '../../constants.js';

const _clearColor = /*@__PURE__*/ new Color();

// cubeToUV() maps a 3D direction vector suitable for cube texture mapping to a 2D
// vector suitable for 2D texture mapping. This code uses the following layout for the
// 2D texture:
//
// xzXZ
//  y Y
//
// Y - Positive y direction
// y - Negative y direction
// X - Positive x direction
// x - Negative x direction
// Z - Positive z direction
// z - Negative z direction
//
// Source and test bed:
// https://gist.github.com/tschw/da10c43c467ce8afd0c4

export const cubeToUV = /*@__PURE__*/ Fn( ( [ pos, texelSizeY ] ) => {

	const v = pos.toVar();

	// Number of texels to avoid at the edge of each square

	const absV = abs( v );

	// Intersect unit cube

	const scaleToCube = div( 1.0, max( absV.x, max( absV.y, absV.z ) ) );
	absV.mulAssign( scaleToCube );

	// Apply scale to avoid seams

	// two texels less per square (one texel will do for NEAREST)
	v.mulAssign( scaleToCube.mul( texelSizeY.mul( 2 ).oneMinus() ) );

	// Unwrap

	// space: -1 ... 1 range for each square
	//
	// #X##		dim    := ( 4 , 2 )
	//  # #		center := ( 1 , 1 )

	const planar = vec2( v.xy ).toVar();

	const almostATexel = texelSizeY.mul( 1.5 );
	const almostOne = almostATexel.oneMinus();

	If( absV.z.greaterThanEqual( almostOne ), () => {

		If( v.z.greaterThan( 0.0 ), () => {

			planar.x.assign( sub( 4.0, v.x ) );

		} );

	} ).ElseIf( absV.x.greaterThanEqual( almostOne ), () => {

		const signX = sign( v.x );
		planar.x.assign( v.z.mul( signX ).add( signX.mul( 2.0 ) ) );

	} ).ElseIf( absV.y.greaterThanEqual( almostOne ), () => {

		const signY = sign( v.y );
		planar.x.assign( v.x.add( signY.mul( 2.0 ) ).add( 2.0 ) );
		planar.y.assign( v.z.mul( signY ).sub( 2.0 ) );

	} );

	// Transform to UV space

	// scale := 0.5 / dim
	// translate := ( center + 0.5 ) / dim
	return vec2( 0.125, 0.25 ).mul( planar ).add( vec2( 0.375, 0.75 ) ).flipY();

} ).setLayout( {
	name: 'cubeToUV',
	type: 'vec2',
	inputs: [
		{ name: 'pos', type: 'vec3' },
		{ name: 'texelSizeY', type: 'float' }
	]
} );

export const BasicPointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, texelSize } ) => {

	return texture( depthTexture, cubeToUV( bd3D, texelSize.y ) ).compare( dp );

} );

export const PointShadowFilter = /*@__PURE__*/ Fn( ( { depthTexture, bd3D, dp, texelSize, shadow } ) => {

	const radius = reference( 'radius', 'float', shadow ).setGroup( renderGroup );
	const offset = vec2( - 1.0, 1.0 ).mul( radius ).mul( texelSize.y );

	return texture( depthTexture, cubeToUV( bd3D.add( offset.xyy ), texelSize.y ) ).compare( dp )
		.add( texture( depthTexture, cubeToUV( bd3D.add( offset.yyy ), texelSize.y ) ).compare( dp ) )
		.add( texture( depthTexture, cubeToUV( bd3D.add( offset.xyx ), texelSize.y ) ).compare( dp ) )
		.add( texture( depthTexture, cubeToUV( bd3D.add( offset.yyx ), texelSize.y ) ).compare( dp ) )
		.add( texture( depthTexture, cubeToUV( bd3D, texelSize.y ) ).compare( dp ) )
		.add( texture( depthTexture, cubeToUV( bd3D.add( offset.xxy ), texelSize.y ) ).compare( dp ) )
		.add( texture( depthTexture, cubeToUV( bd3D.add( offset.yxy ), texelSize.y ) ).compare( dp ) )
		.add( texture( depthTexture, cubeToUV( bd3D.add( offset.xxx ), texelSize.y ) ).compare( dp ) )
		.add( texture( depthTexture, cubeToUV( bd3D.add( offset.yxx ), texelSize.y ) ).compare( dp ) )
		.mul( 1.0 / 9.0 );

} );

const pointShadowFilter = /*@__PURE__*/ Fn( ( { filterFn, depthTexture, shadowCoord, shadow } ) => {

	// for point lights, the uniform @vShadowCoord is re-purposed to hold
	// the vector from the light to the world-space position of the fragment.
	const lightToPosition = shadowCoord.xyz.toVar();
	const lightToPositionLength = lightToPosition.length();

	const cameraNearLocal = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.near );
	const cameraFarLocal = uniform( 'float' ).setGroup( renderGroup ).onRenderUpdate( () => shadow.camera.far );
	const bias = reference( 'bias', 'float', shadow ).setGroup( renderGroup );
	const mapSize = uniform( shadow.mapSize ).setGroup( renderGroup );

	const result = float( 1.0 ).toVar();

	If( lightToPositionLength.sub( cameraFarLocal ).lessThanEqual( 0.0 ).and( lightToPositionLength.sub( cameraNearLocal ).greaterThanEqual( 0.0 ) ), () => {

		// dp = normalized distance from light to fragment position
		const dp = lightToPositionLength.sub( cameraNearLocal ).div( cameraFarLocal.sub( cameraNearLocal ) ).toVar(); // need to clamp?
		dp.addAssign( bias );

		// bd3D = base direction 3D
		const bd3D = lightToPosition.normalize();
		const texelSize = vec2( 1.0 ).div( mapSize.mul( vec2( 4.0, 2.0 ) ) );

		// percentage-closer filtering
		result.assign( filterFn( { depthTexture, bd3D, dp, texelSize, shadow } ) );

	} );

	return result;

} );

const _viewport = /*@__PURE__*/ new Vector4();
const _viewportSize = /*@__PURE__*/ new Vector2();
const _shadowMapSize = /*@__PURE__*/ new Vector2();


/**
 * Represents the shadow implementation for point light nodes.
 *
 * @augments ShadowNode
 */
class PointShadowNode extends ShadowNode {

	static get type() {

		return 'PointShadowNode';

	}

	/**
	 * Constructs a new point shadow node.
	 *
	 * @param {PointLight} light - The shadow casting point light.
	 * @param {?PointLightShadow} [shadow=null] - An optional point light shadow.
	 */
	constructor( light, shadow = null ) {

		super( light, shadow );

	}

	/**
	 * Overwrites the default implementation to return point light shadow specific
	 * filtering functions.
	 *
	 * @param {number} type - The shadow type.
	 * @return {Function} The filtering function.
	 */
	getShadowFilterFn( type ) {

		return type === BasicShadowMap ? BasicPointShadowFilter : PointShadowFilter;

	}

	/**
	 * Overwrites the default implementation so the unaltered shadow position is used.
	 *
	 * @param {NodeBuilder} builder - A reference to the current node builder.
	 * @param {Node<vec3>} shadowPosition - A node representing the shadow position.
	 * @return {Node<vec3>} The shadow coordinates.
	 */
	setupShadowCoord( builder, shadowPosition ) {

		return shadowPosition;

	}

	/**
	 * Overwrites the default implementation to only use point light specific
	 * shadow filter functions.
	 *
	 * @param {NodeBuilder} builder - A reference to the current node builder.
	 * @param {Object} inputs - A configuration object that defines the shadow filtering.
	 * @param {Function} inputs.filterFn - This function defines the filtering type of the shadow map e.g. PCF.
	 * @param {Texture} inputs.shadowTexture - A reference to the shadow map's texture.
	 * @param {DepthTexture} inputs.depthTexture - A reference to the shadow map's texture data.
	 * @param {Node<vec3>} inputs.shadowCoord - Shadow coordinates which are used to sample from the shadow map.
	 * @param {LightShadow} inputs.shadow - The light shadow.
	 * @return {Node<float>} The result node of the shadow filtering.
	 */
	setupShadowFilter( builder, { filterFn, shadowTexture, depthTexture, shadowCoord, shadow } ) {

		return pointShadowFilter( { filterFn, shadowTexture, depthTexture, shadowCoord, shadow } );

	}

	/**
	 * Overwrites the default implementation with point light specific
	 * rendering code.
	 *
	 * @param {NodeFrame} frame - A reference to the current node frame.
	 */
	renderShadow( frame ) {

		const { shadow, shadowMap, light } = this;
		const { renderer, scene } = frame;

		const shadowFrameExtents = shadow.getFrameExtents();

		_shadowMapSize.copy( shadow.mapSize );
		_shadowMapSize.multiply( shadowFrameExtents );

		shadowMap.setSize( _shadowMapSize.width, _shadowMapSize.height );

		_viewportSize.copy( shadow.mapSize );

		//

		const previousAutoClear = renderer.autoClear;

		const previousClearColor = renderer.getClearColor( _clearColor );
		const previousClearAlpha = renderer.getClearAlpha();

		renderer.autoClear = false;
		renderer.setClearColor( shadow.clearColor, shadow.clearAlpha );
		renderer.clear();

		const viewportCount = shadow.getViewportCount();

		for ( let vp = 0; vp < viewportCount; vp ++ ) {

			const viewport = shadow.getViewport( vp );

			const x = _viewportSize.x * viewport.x;
			const y = _shadowMapSize.y - _viewportSize.y - ( _viewportSize.y * viewport.y );

			_viewport.set(
				x,
				y,
				_viewportSize.x * viewport.z,
				_viewportSize.y * viewport.w
			);

			shadowMap.viewport.copy( _viewport );

			shadow.updateMatrices( light, vp );

			renderer.render( scene, shadow.camera );

		}

		//

		renderer.autoClear = previousAutoClear;
		renderer.setClearColor( previousClearColor, previousClearAlpha );

	}

}

export default PointShadowNode;

/**
 * TSL function for creating an instance of `PointShadowNode`.
 *
 * @tsl
 * @function
 * @param {PointLight} light - The shadow casting point light.
 * @param {?PointLightShadow} [shadow=null] - An optional point light shadow.
 * @return {PointShadowNode} The created point shadow node.
 */
export const pointShadow = ( light, shadow ) => nodeObject( new PointShadowNode( light, shadow ) );
