import LightingModel from '../core/LightingModel.js';
import { property } from '../core/PropertyNode.js';
import { float, If, uniform, vec3, vec4 } from '../tsl/TSLBase.js';
import { positionWorld } from '../accessors/Position.js';
import { cameraFar, cameraNear, cameraPosition, cameraViewMatrix } from '../accessors/Camera.js';
import { Loop } from '../utils/LoopNode.js';
import { linearDepth, viewZToPerspectiveDepth } from '../display/ViewportDepthNode.js';
import { modelRadius } from '../accessors/ModelNode.js';
import { LTC_Evaluate_Volume } from './BSDF/LTC.js';

const scatteringDensity = property( 'vec3' );
const linearDepthRay = property( 'vec3' );
const outgoingRayLight = property( 'vec3' );

/**
 * VolumetricLightingModel class extends the LightingModel to implement volumetric lighting effects.
 * This model calculates the scattering and transmittance of light through a volumetric medium.
 * It dynamically adjusts the direction of the ray based on the camera and object positions.
 * The model supports custom scattering and depth nodes to enhance the lighting effects.
 *
 * @augments LightingModel
 */
class VolumetricLightingModel extends LightingModel {

	constructor() {

		super();

	}

	start( builder ) {

		const { material, context } = builder;

		const startPos = property( 'vec3' );
		const endPos = property( 'vec3' );

		// This approach dynamically changes the direction of the ray,
		// prioritizing the ray from the camera to the object if it is inside the mesh, and from the object to the camera if it is far away.

		If( cameraPosition.sub( positionWorld ).length().greaterThan( modelRadius.mul( 2 ) ), () => {

			startPos.assign( cameraPosition );
			endPos.assign( positionWorld );

		} ).Else( () => {

			startPos.assign( positionWorld );
			endPos.assign( cameraPosition );

		} );

		//

		const viewVector = endPos.sub( startPos );

		const steps = uniform( 'int' ).onRenderUpdate( ( { material } ) => material.steps );
		const stepSize = viewVector.length().div( steps ).toVar();

		const rayDir = viewVector.normalize().toVar(); // TODO: toVar() should be automatic here ( in loop )

		const distTravelled = float( 0.0 ).toVar();
		const transmittance = vec3( 1 ).toVar();

		if ( material.offsetNode ) {

			// reduce banding

			distTravelled.addAssign( material.offsetNode.mul( stepSize ) );

		}

		Loop( steps, () => {

			const positionRay = startPos.add( rayDir.mul( distTravelled ) );
			const positionViewRay = cameraViewMatrix.mul( vec4( positionRay, 1 ) ).xyz;

			if ( material.depthNode !== null ) {

				linearDepthRay.assign( linearDepth( viewZToPerspectiveDepth( positionViewRay.z, cameraNear, cameraFar ) ) );

				context.sceneDepthNode = linearDepth( material.depthNode ).toVar();

			}

			context.positionWorld = positionRay;
			context.shadowPositionWorld = positionRay;
			context.positionView = positionViewRay;

			scatteringDensity.assign( 0 );

			let scatteringNode;

			if ( material.scatteringNode ) {

				scatteringNode = material.scatteringNode( {
					positionRay
				} );

			}

			super.start( builder );

			if ( scatteringNode ) {

				scatteringDensity.mulAssign( scatteringNode );

			}

			// beer's law

			const falloff = scatteringDensity.mul( .01 ).negate().mul( stepSize ).exp();
			transmittance.mulAssign( falloff );

			// move along the ray

			distTravelled.addAssign( stepSize );

		} );

		outgoingRayLight.addAssign( transmittance.saturate().oneMinus() );

	}

	scatteringLight( lightColor, builder ) {

		const sceneDepthNode = builder.context.sceneDepthNode;

		if ( sceneDepthNode ) {

			If( sceneDepthNode.greaterThanEqual( linearDepthRay ), () => {

				scatteringDensity.addAssign( lightColor );

			} );

		} else {

			scatteringDensity.addAssign( lightColor );

		}

	}

	direct( { lightNode, lightColor }, builder ) {

		// Ignore lights with infinite distance

		if ( lightNode.light.distance === undefined ) return;

		// TODO: We need a viewportOpaque*() ( output, depth ) to fit with modern rendering approaches

		const directLight = lightColor.xyz.toVar();
		directLight.mulAssign( lightNode.shadowNode ); // it no should be necessary if used in the same render pass

		this.scatteringLight( directLight, builder );

	}

	directRectArea( { lightColor, lightPosition, halfWidth, halfHeight }, builder ) {

		const p0 = lightPosition.add( halfWidth ).sub( halfHeight ); // counterclockwise; light shines in local neg z direction
		const p1 = lightPosition.sub( halfWidth ).sub( halfHeight );
		const p2 = lightPosition.sub( halfWidth ).add( halfHeight );
		const p3 = lightPosition.add( halfWidth ).add( halfHeight );

		const P = builder.context.positionView;

		const directLight = lightColor.xyz.mul( LTC_Evaluate_Volume( { P, p0, p1, p2, p3 } ) ).pow( 1.5 );

		this.scatteringLight( directLight, builder );

	}

	finish( builder ) {

		builder.context.outgoingLight.assign( outgoingRayLight );

	}

}

export default VolumetricLightingModel;
