import Node from './Node.js';
import { addMethodChaining } from '../tsl/TSLCore.js';
import { warn } from '../../utils.js';

/**
 * This node can be used as a context management component for another node.
 * {@link NodeBuilder} performs its node building process in a specific context and
 * this node allows the modify the context. A typical use case is to overwrite `getUV()` e.g.:
 *
 * ```js
 *node.context( { getUV: () => customCoord } );
 *\// or
 *material.contextNode = context( { getUV: () => customCoord } );
 *\// or
 *renderer.contextNode = context( { getUV: () => customCoord } );
 *\// or
 *scenePass.contextNode = context( { getUV: () => customCoord } );
 *```
 * @augments Node
 */
class ContextNode extends Node {

	static get type() {

		return 'ContextNode';

	}

	/**
	 * Constructs a new context node.
	 *
	 * @param {Node} node - The node whose context should be modified.
	 * @param {Object} [value={}] - The modified context data.
	 */
	constructor( node = null, value = {} ) {

		super();

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

		/**
		 * The node whose context should be modified.
		 *
		 * @type {Node}
		 */
		this.node = node;

		/**
		 * The modified context data.
		 *
		 * @type {Object}
		 * @default {}
		 */
		this.value = value;

	}

	/**
	 * This method is overwritten to ensure it returns the reference to {@link ContextNode#node}.
	 *
	 * @return {Node} A reference to {@link ContextNode#node}.
	 */
	getScope() {

		return this.node.getScope();

	}

	/**
	 * This method is overwritten to ensure it returns the type of {@link ContextNode#node}.
	 *
	 * @param {NodeBuilder} builder - The current node builder.
	 * @return {string} The node type.
	 */
	getNodeType( builder ) {

		return this.node.getNodeType( builder );

	}

	/**
	 * Gathers the context data from all parent context nodes.
	 *
	 * @return {Object} The gathered context data.
	 */
	getFlowContextData() {

		const children = [];

		this.traverse( ( node ) => {

			if ( node.isContextNode === true ) {

				children.push( node.value );

			}

		} );

		return Object.assign( {}, ...children );

	}

	/**
	 * This method is overwritten to ensure it returns the member type of {@link ContextNode#node}.
	 *
	 * @param {NodeBuilder} builder - The current node builder.
	 * @param {string} name - The member name.
	 * @returns {string} The member type.
	 */
	getMemberType( builder, name ) {

		return this.node.getMemberType( builder, name );

	}

	analyze( builder ) {

		const previousContext = builder.addContext( this.value );

		this.node.build( builder );

		builder.setContext( previousContext );

	}

	setup( builder ) {

		const previousContext = builder.addContext( this.value );

		this.node.build( builder );

		builder.setContext( previousContext );

	}

	generate( builder, output ) {

		const previousContext = builder.addContext( this.value );

		const snippet = this.node.build( builder, output );

		builder.setContext( previousContext );

		return snippet;

	}

}

export default ContextNode;

/**
 * TSL function for creating a context node.
 *
 * @tsl
 * @function
 * @param {Node|Object} [nodeOrValue={}] - The node whose context should be modified or the modified context data.
 * @param {Object} [value={}] - The modified context data.
 * @returns {ContextNode}
 */
export const context = ( nodeOrValue = null, value = {} ) => {

	let node = nodeOrValue;

	if ( node === null || node.isNode !== true ) {

		value = node || value;
		node = null;

	}

	return new ContextNode( node, value );

};

/**
 * TSL function for defining a uniformFlow context value for a given node.
 *
 * @tsl
 * @function
 * @param {Node} node - The node whose dependencies should all execute within a uniform control-flow path.
 * @returns {ContextNode}
 */
export const uniformFlow = ( node ) => context( node, { uniformFlow: true } );

/**
 * TSL function for defining a name for the context value for a given node.
 *
 * @tsl
 * @function
 * @param {Node} node - The node whose context should be modified.
 * @param {string} name - The name to set.
 * @returns {ContextNode}
 */
export const setName = ( node, name ) => context( node, { nodeName: name } );

/**
 * TSL function for defining a built-in shadow context for a given node.
 *
 * @tsl
 * @function
 * @param {ShadowNode} shadowNode - The shadow node representing the light's shadow.
 * @param {Light} light - The light associated with the shadow.
 * @param {Node} [node=null] - The node whose context should be modified.
 * @returns {ContextNode}
 */
export function builtinShadowContext( shadowNode, light, node = null ) {

	return context( node, {

		getShadow: ( { light: shadowLight, shadowColorNode } ) => {

			if ( light === shadowLight ) {

				return shadowColorNode.mul( shadowNode );

			}

			return shadowColorNode;

		}

	} );

}

/**
 * TSL function for defining a built-in ambient occlusion context for a given node.
 *
 * @tsl
 * @function
 * @param {Node} aoNode - The ambient occlusion value node to apply.
 * @param {Node} [node=null] - The node whose context should be modified.
 * @returns {ContextNode}
 */
export function builtinAOContext( aoNode, node = null ) {

	return context( node, {

		getAO: ( inputNode, { material } ) => {

			if ( material.transparent === true ) return inputNode;

			return inputNode !== null ? inputNode.mul( aoNode ) : aoNode;

		}

	} );

}

/**
 * TSL function for defining a label context value for a given node.
 *
 * @tsl
 * @function
 * @deprecated
 * @param {Node} node - The node whose context should be modified.
 * @param {string} name - The name/label to set.
 * @returns {ContextNode}
 */
export function label( node, name ) {

	warn( 'TSL: "label()" has been deprecated. Use "setName()" instead.' ); // @deprecated r179

	return setName( node, name );

}

addMethodChaining( 'context', context );
addMethodChaining( 'label', label );
addMethodChaining( 'uniformFlow', uniformFlow );
addMethodChaining( 'setName', setName );
addMethodChaining( 'builtinShadowContext', ( node, shadowNode, light ) => builtinShadowContext( shadowNode, light, node ) );
addMethodChaining( 'builtinAOContext', ( node, aoValue ) => builtinAOContext( aoValue, node ) );
