import LightingNode from './LightingNode.js';
import { NodeUpdateType } from '../core/constants.js';
import { uniform } from '../core/UniformNode.js';
import { Color } from '../../math/Color.js';
import { renderGroup } from '../core/UniformGroupNode.js';
import { shadow } from './ShadowNode.js';
import { nodeObject } from '../tsl/TSLCore.js';
import { lightViewPosition } from '../accessors/Lights.js';
import { positionView } from '../accessors/Position.js';

/**
 * Base class for analytic light nodes.
 *
 * @augments LightingNode
 */
class AnalyticLightNode extends LightingNode {

	static get type() {

		return 'AnalyticLightNode';

	}

	/**
	 * Constructs a new analytic light node.
	 *
	 * @param {?Light} [light=null] - The light source.
	 */
	constructor( light = null ) {

		super();

		/**
		 * The light source.
		 *
		 * @type {?Light}
		 * @default null
		 */
		this.light = light;

		/**
		 * The light's color value.
		 *
		 * @type {Color}
		 */
		this.color = new Color();

		/**
		 * The light's color node. Points to `colorNode` of the light source, if set. Otherwise
		 * it creates a uniform node based on {@link AnalyticLightNode#color}.
		 *
		 * @type {Node}
		 */
		this.colorNode = ( light && light.colorNode ) || uniform( this.color ).setGroup( renderGroup );

		/**
		 * This property is used to retain a reference to the original value of {@link AnalyticLightNode#colorNode}.
		 * The final color node is represented by a different node when using shadows.
		 *
		 * @type {?Node}
		 * @default null
		 */
		this.baseColorNode = null;

		/**
		 * Represents the light's shadow.
		 *
		 * @type {?ShadowNode}
   		 * @default null
		 */
		this.shadowNode = null;

		/**
		 * Represents the light's shadow color.
		 *
		 * @type {?Node}
   		 * @default null
		 */
		this.shadowColorNode = null;

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isAnalyticLightNode = true;

		/**
		 * Overwritten since analytic light nodes are updated
		 * once per frame.
		 *
		 * @type {string}
		 * @default 'frame'
		 */
		this.updateType = NodeUpdateType.FRAME;

		if ( light && light.shadow ) {

			this._shadowDisposeListener = () => {

				this.disposeShadow();

			};

			light.addEventListener( 'dispose', this._shadowDisposeListener );

		}

	}

	dispose() {

		if ( this._shadowDisposeListener ) {

			this.light.removeEventListener( 'dispose', this._shadowDisposeListener );

		}

		super.dispose();

	}

	/**
	 * Frees internal resources related to shadows.
	 */
	disposeShadow() {

		if ( this.shadowNode !== null ) {

			this.shadowNode.dispose();
			this.shadowNode = null;

		}

		this.shadowColorNode = null;

		if ( this.baseColorNode !== null ) {

			this.colorNode = this.baseColorNode;
			this.baseColorNode = null;

		}

	}

	getHash() {

		return this.light.uuid;

	}

	/**
	 * Returns a node representing a direction vector which points from the current
	 * position in view space to the light's position in view space.
	 *
	 * @param {NodeBuilder} builder - The builder object used for setting up the light.
	 * @return {Node<vec3>} The light vector node.
	 */
	getLightVector( builder ) {

		return lightViewPosition( this.light ).sub( builder.context.positionView || positionView );

	}

	/**
	 * Sets up the direct lighting for the analytic light node.
	 *
	 * @abstract
	 * @param {NodeBuilder} builder - The builder object used for setting up the light.
	 * @return {Object|undefined} The direct light data (color and direction).
	 */
	setupDirect( /*builder*/ ) { }

	/**
	 * Sets up the direct rect area lighting for the analytic light node.
	 *
	 * @abstract
	 * @param {NodeBuilder} builder - The builder object used for setting up the light.
	 * @return {Object|undefined} The direct rect area light data.
	 */
	setupDirectRectArea( /*builder*/ ) { }

	/**
	 * Setups the shadow node for this light. The method exists so concrete light classes
	 * can setup different types of shadow nodes.
	 *
	 * @return {ShadowNode} The created shadow node.
	 */
	setupShadowNode() {

		return shadow( this.light );

	}

	/**
	 * Setups the shadow for this light. This method is only executed if the light
	 * cast shadows and the current build object receives shadows. It incorporates
	 * shadows into the lighting computation.
	 *
	 * @param {NodeBuilder} builder - The current node builder.
	 */
	setupShadow( builder ) {

		const { renderer } = builder;

		if ( renderer.shadowMap.enabled === false ) return;

		let shadowColorNode = this.shadowColorNode;

		if ( shadowColorNode === null ) {

			const customShadowNode = this.light.shadow.shadowNode;

			let shadowNode;

			if ( customShadowNode !== undefined ) {

				shadowNode = nodeObject( customShadowNode );

			} else {

				shadowNode = this.setupShadowNode();

			}

			this.shadowNode = shadowNode;

			this.shadowColorNode = shadowColorNode = this.colorNode.mul( shadowNode );

			this.baseColorNode = this.colorNode;

		}

		//

		if ( builder.context.getShadow ) {

			shadowColorNode = builder.context.getShadow( this, builder );

		}

		this.colorNode = shadowColorNode;

	}

	/**
	 * Unlike most other nodes, lighting nodes do not return a output node in {@link Node#setup}.
	 * The main purpose of lighting nodes is to configure the current {@link LightingModel} and/or
	 * invocate the respective interface methods.
	 *
	 * @param {NodeBuilder} builder - The current node builder.
	 */
	setup( builder ) {

		this.colorNode = this.baseColorNode || this.colorNode;

		if ( this.light.castShadow ) {

			if ( builder.object.receiveShadow ) {

				this.setupShadow( builder );

			}

		} else if ( this.shadowNode !== null ) {

			this.shadowNode.dispose();
			this.shadowNode = null;
			this.shadowColorNode = null;

		}

		const directLightData = this.setupDirect( builder );
		const directRectAreaLightData = this.setupDirectRectArea( builder );

		if ( directLightData ) {

			builder.lightsNode.setupDirectLight( builder, this, directLightData );

		}

		if ( directRectAreaLightData ) {

			builder.lightsNode.setupDirectRectAreaLight( builder, this, directRectAreaLightData );

		}

	}

	/**
	 * The update method is used to update light uniforms per frame.
	 * Potentially overwritten in concrete light nodes to update light
	 * specific uniforms.
	 *
	 * @param {NodeFrame} frame - A reference to the current node frame.
	 */
	update( /*frame*/ ) {

		const { light } = this;

		this.color.copy( light.color ).multiplyScalar( light.intensity );

	}

}

export default AnalyticLightNode;
