import {
	UniformsLib,
	UniformsUtils
} from 'three';

import { Node } from '../../core/Node.js';
import { ExpressionNode } from '../../core/ExpressionNode.js';
import { ColorNode } from '../../inputs/ColorNode.js';
import { FloatNode } from '../../inputs/FloatNode.js';
import { SpecularMIPLevelNode } from '../../utils/SpecularMIPLevelNode.js';

class StandardNode extends Node {

	constructor() {

		super();

		this.color = new ColorNode( 0xFFFFFF );
		this.roughness = new FloatNode( 1 );
		this.metalness = new FloatNode( 0 );

	}

	build( builder ) {

		let code;

		builder.define( 'STANDARD' );

		const useClearcoat = this.clearcoat || this.clearcoatRoughness || this.clearCoatNormal;

		if ( useClearcoat ) {

			builder.define( 'CLEARCOAT' );

		}

		builder.requires.lights = true;

		builder.extensions.derivatives = true;
		builder.extensions.shaderTextureLOD = true;

		if ( builder.isShader( 'vertex' ) ) {

			const position = this.position ? this.position.analyzeAndFlow( builder, 'v3', { cache: 'position' } ) : undefined;

			builder.mergeUniform( UniformsUtils.merge( [

				UniformsLib.fog,
				UniformsLib.lights

			] ) );

			if ( UniformsLib.LTC_1 ) {

				// add ltc data textures to material uniforms

				builder.uniforms.ltc_1 = { value: undefined };
				builder.uniforms.ltc_2 = { value: undefined };

			}

			builder.addParsCode( /* glsl */`
				varying vec3 vViewPosition;

				#ifndef FLAT_SHADED

					varying vec3 vNormal;

				#endif

				//"#include <encodings_pars_fragment> // encoding functions
				#include <fog_pars_vertex>
				#include <morphtarget_pars_vertex>
				#include <skinning_pars_vertex>
				#include <shadowmap_pars_vertex>
				#include <logdepthbuf_pars_vertex>
				#include <clipping_planes_pars_vertex>`

			);

			const output = [
				'#include <beginnormal_vertex>',
				'#include <morphnormal_vertex>',
				'#include <skinbase_vertex>',
				'#include <skinnormal_vertex>',
				'#include <defaultnormal_vertex>',

				'#ifndef FLAT_SHADED', // Normal computed with derivatives when FLAT_SHADED

				'	vNormal = normalize( transformedNormal );',

				'#endif',

				'#include <begin_vertex>'
			];

			if ( position ) {

				output.push(
					position.code,
					position.result ? 'transformed = ' + position.result + ';' : ''
				);

			}

			output.push(
				'#include <morphtarget_vertex>',
				'#include <skinning_vertex>',
				'#include <project_vertex>',
				'#include <fog_vertex>',
				'#include <logdepthbuf_vertex>',
				'#include <clipping_planes_vertex>',

				'	vViewPosition = - mvPosition.xyz;',

				'#include <worldpos_vertex>',
				'#include <shadowmap_vertex>'
			);

			code = output.join( '\n' );

		} else {

			const specularRoughnessNode = new ExpressionNode( 'material.specularRoughness', 'f' );
			const clearcoatRoughnessNode = new ExpressionNode( 'material.clearcoatRoughness', 'f' );

			const contextEnvironment = {
				roughness: specularRoughnessNode,
				bias: new SpecularMIPLevelNode( specularRoughnessNode ),
				viewNormal: new ExpressionNode( 'normal', 'v3' ),
				worldNormal: new ExpressionNode( 'inverseTransformDirection( geometry.normal, viewMatrix )', 'v3' ),
				gamma: true
			};

			const contextGammaOnly = {
				gamma: true
			};

			const contextClearcoatEnvironment = {
				roughness: clearcoatRoughnessNode,
				bias: new SpecularMIPLevelNode( clearcoatRoughnessNode ),
				viewNormal: new ExpressionNode( 'clearcoatNormal', 'v3' ),
				worldNormal: new ExpressionNode( 'inverseTransformDirection( geometry.clearcoatNormal, viewMatrix )', 'v3' ),
				gamma: true
			};

			// analyze all nodes to reuse generate codes

			if ( this.mask ) this.mask.analyze( builder );

			this.color.analyze( builder, { slot: 'color', context: contextGammaOnly } );
			this.roughness.analyze( builder );
			this.metalness.analyze( builder );

			if ( this.alpha ) this.alpha.analyze( builder );

			if ( this.normal ) this.normal.analyze( builder );

			if ( this.clearcoat ) this.clearcoat.analyze( builder );
			if ( this.clearcoatRoughness ) this.clearcoatRoughness.analyze( builder );
			if ( this.clearcoatNormal ) this.clearcoatNormal.analyze( builder );

			if ( this.reflectivity ) this.reflectivity.analyze( builder );

			if ( this.light ) this.light.analyze( builder, { cache: 'light' } );

			if ( this.ao ) this.ao.analyze( builder );
			if ( this.ambient ) this.ambient.analyze( builder );
			if ( this.shadow ) this.shadow.analyze( builder );
			if ( this.emissive ) this.emissive.analyze( builder, { slot: 'emissive' } );

			if ( this.environment ) {

				// isolate environment from others inputs ( see TextureNode, CubeTextureNode )
				// environment.analyze will detect if there is a need of calculate irradiance

				this.environment.analyze( builder, { cache: 'radiance', context: contextEnvironment, slot: 'radiance' } );

				if ( builder.requires.irradiance ) {

					this.environment.analyze( builder, { cache: 'irradiance', context: contextEnvironment, slot: 'irradiance' } );

				}

			}

			if ( this.sheen ) this.sheen.analyze( builder );

			// build code

			const mask = this.mask ? this.mask.flow( builder, 'b' ) : undefined;

			const color = this.color.flow( builder, 'c', { slot: 'color', context: contextGammaOnly } );
			const roughness = this.roughness.flow( builder, 'f' );
			const metalness = this.metalness.flow( builder, 'f' );

			const alpha = this.alpha ? this.alpha.flow( builder, 'f' ) : undefined;

			const normal = this.normal ? this.normal.flow( builder, 'v3' ) : undefined;

			const clearcoat = this.clearcoat ? this.clearcoat.flow( builder, 'f' ) : undefined;
			const clearcoatRoughness = this.clearcoatRoughness ? this.clearcoatRoughness.flow( builder, 'f' ) : undefined;
			const clearcoatNormal = this.clearcoatNormal ? this.clearcoatNormal.flow( builder, 'v3' ) : undefined;

			const reflectivity = this.reflectivity ? this.reflectivity.flow( builder, 'f' ) : undefined;

			const light = this.light ? this.light.flow( builder, 'v3', { cache: 'light' } ) : undefined;

			const ao = this.ao ? this.ao.flow( builder, 'f' ) : undefined;
			const ambient = this.ambient ? this.ambient.flow( builder, 'c' ) : undefined;
			const shadow = this.shadow ? this.shadow.flow( builder, 'c' ) : undefined;
			const emissive = this.emissive ? this.emissive.flow( builder, 'c', { slot: 'emissive' } ) : undefined;

			let environment;

			if ( this.environment ) {

				environment = {
					radiance: this.environment.flow( builder, 'c', { cache: 'radiance', context: contextEnvironment, slot: 'radiance' } )
				};

				if ( builder.requires.irradiance ) {

					environment.irradiance = this.environment.flow( builder, 'c', { cache: 'irradiance', context: contextEnvironment, slot: 'irradiance' } );

				}

			}

			const clearcoatEnv = useClearcoat && environment ? this.environment.flow( builder, 'c', { cache: 'clearcoat', context: contextClearcoatEnvironment, slot: 'environment' } ) : undefined;

			const sheen = this.sheen ? this.sheen.flow( builder, 'c' ) : undefined;

			builder.requires.transparent = alpha !== undefined;

			builder.addParsCode( /* glsl */`
				varying vec3 vViewPosition;

				#ifndef FLAT_SHADED

					varying vec3 vNormal;

				#endif

				#include <dithering_pars_fragment>
				#include <fog_pars_fragment>
				#include <bsdfs>
				#include <lights_pars_begin>
				#include <lights_physical_pars_fragment>
				#include <shadowmap_pars_fragment>
				#include <logdepthbuf_pars_fragment>`
			);

			const output = [
				'#include <clipping_planes_fragment>',

				// add before: prevent undeclared normal
				'	#include <normal_fragment_begin>',
				'	#include <clearcoat_normal_fragment_begin>',

				// add before: prevent undeclared material
				'	PhysicalMaterial material;',
				'	material.diffuseColor = vec3( 1.0 );'
			];

			if ( mask ) {

				output.push(
					mask.code,
					'if ( ! ' + mask.result + ' ) discard;'
				);

			}

			output.push(
				color.code,
				'	vec3 diffuseColor = ' + color.result + ';',
				'	ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );',

				'#include <logdepthbuf_fragment>',

				roughness.code,
				'	float roughnessFactor = ' + roughness.result + ';',

				metalness.code,
				'	float metalnessFactor = ' + metalness.result + ';'
			);

			if ( alpha ) {

				output.push(
					alpha.code,
					'#ifdef ALPHATEST',

					'	if ( ' + alpha.result + ' <= ALPHATEST ) discard;',

					'#endif'
				);

			}

			if ( normal ) {

				output.push(
					normal.code,
					'normal = ' + normal.result + ';'
				);

			}

			if ( clearcoatNormal ) {

				output.push(
					clearcoatNormal.code,
					'clearcoatNormal = ' + clearcoatNormal.result + ';'
				);

			}

			// anti-aliasing code by @elalish

			output.push(
				'vec3 dxy = max( abs( dFdx( geometryNormal ) ), abs( dFdy( geometryNormal ) ) );',
				'float geometryRoughness = max( max( dxy.x, dxy.y ), dxy.z );',
			);

			// optimization for now

			output.push(
				'material.diffuseColor = ' + ( light ? 'vec3( 1.0 )' : 'diffuseColor * ( 1.0 - metalnessFactor )' ) + ';',

				'material.specularRoughness = max( roughnessFactor, 0.0525 );',
				'material.specularRoughness += geometryRoughness;',
				'material.specularRoughness = min( material.specularRoughness, 1.0 );',

				'material.specularRoughness = clamp( roughnessFactor, 0.04, 1.0 );'
			);

			if ( clearcoat ) {

				output.push(
					clearcoat.code,
					'material.clearcoat = saturate( ' + clearcoat.result + ' );' // Burley clearcoat model
				);

			} else if ( useClearcoat ) {

				output.push( 'material.clearcoat = 0.0;' );

			}

			if ( clearcoatRoughness ) {

				output.push(
					clearcoatRoughness.code,
					'material.clearcoatRoughness = max( ' + clearcoatRoughness.result + ', 0.0525 );',
					'material.clearcoatRoughness += geometryRoughness;',
					'material.clearcoatRoughness = min( material.clearcoatRoughness, 1.0 );'
				);

			} else if ( useClearcoat ) {

				output.push( 'material.clearcoatRoughness = 0.0;' );

			}

			if ( sheen ) {

				output.push( 'material.sheenColor = ' + sheen.result + ';' );

			}

			if ( reflectivity ) {

				output.push(
					reflectivity.code,
					'material.specularColor = mix( vec3( MAXIMUM_SPECULAR_COEFFICIENT * pow2( ' + reflectivity.result + ' ) ), diffuseColor, metalnessFactor );'
				);

			} else {

				output.push(
					'material.specularColor = mix( vec3( DEFAULT_SPECULAR_COEFFICIENT ), diffuseColor, metalnessFactor );'
				);

			}

			output.push(
				'#include <lights_fragment_begin>'
			);

			if ( light ) {

				output.push(
					light.code,
					'reflectedLight.directDiffuse = ' + light.result + ';'
				);

				// apply color

				output.push(
					'diffuseColor *= 1.0 - metalnessFactor;',

					'reflectedLight.directDiffuse *= diffuseColor;',
					'reflectedLight.indirectDiffuse *= diffuseColor;'
				);

			}

			if ( ao ) {

				output.push(
					ao.code,
					'reflectedLight.indirectDiffuse *= ' + ao.result + ';',
					'float dotNV = saturate( dot( geometry.normal, geometry.viewDir ) );',
					'reflectedLight.indirectSpecular *= computeSpecularOcclusion( dotNV, ' + ao.result + ', material.specularRoughness );'
				);

			}

			if ( ambient ) {

				output.push(
					ambient.code,
					'reflectedLight.indirectDiffuse += ' + ambient.result + ';'
				);

			}

			if ( shadow ) {

				output.push(
					shadow.code,
					'reflectedLight.directDiffuse *= ' + shadow.result + ';',
					'reflectedLight.directSpecular *= ' + shadow.result + ';'
				);

			}

			if ( emissive ) {

				output.push(
					emissive.code,
					'reflectedLight.directDiffuse += ' + emissive.result + ';'
				);

			}

			if ( environment ) {

				output.push( environment.radiance.code );

				if ( builder.requires.irradiance ) {

					output.push( environment.irradiance.code );

				}

				if ( clearcoatEnv ) {

					output.push(
						clearcoatEnv.code,
						'clearcoatRadiance += ' + clearcoatEnv.result + ';'
					);

				}

				output.push( 'radiance += ' + environment.radiance.result + ';' );

				if ( builder.requires.irradiance ) {

					output.push( 'iblIrradiance += PI * ' + environment.irradiance.result + ';' );

				}

			}

			output.push(
				'#include <lights_fragment_end>'
			);

			output.push( 'vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular;' );

			if ( alpha ) {

				output.push( 'gl_FragColor = vec4( outgoingLight, ' + alpha.result + ' );' );

			} else {

				output.push( 'gl_FragColor = vec4( outgoingLight, 1.0 );' );

			}

			output.push(
				'#include <tonemapping_fragment>',
				'#include <encodings_fragment>',
				'#include <fog_fragment>',
				'#include <premultiplied_alpha_fragment>',
				'#include <dithering_fragment>'
			);

			code = output.join( '\n' );

		}

		return code;

	}

	copy( source ) {

		super.copy( source );

		// vertex

		if ( source.position ) this.position = source.position;

		// fragment

		this.color = source.color;
		this.roughness = source.roughness;
		this.metalness = source.metalness;

		if ( source.mask ) this.mask = source.mask;

		if ( source.alpha ) this.alpha = source.alpha;

		if ( source.normal ) this.normal = source.normal;

		if ( source.clearcoat ) this.clearcoat = source.clearcoat;
		if ( source.clearcoatRoughness ) this.clearcoatRoughness = source.clearcoatRoughness;
		if ( source.clearcoatNormal ) this.clearcoatNormal = source.clearcoatNormal;

		if ( source.reflectivity ) this.reflectivity = source.reflectivity;

		if ( source.light ) this.light = source.light;
		if ( source.shadow ) this.shadow = source.shadow;

		if ( source.ao ) this.ao = source.ao;

		if ( source.emissive ) this.emissive = source.emissive;
		if ( source.ambient ) this.ambient = source.ambient;

		if ( source.environment ) this.environment = source.environment;

		if ( source.sheen ) this.sheen = source.sheen;

		return this;

	}

	toJSON( meta ) {

		let data = this.getJSONNode( meta );

		if ( ! data ) {

			data = this.createJSONNode( meta );

			// vertex

			if ( this.position ) data.position = this.position.toJSON( meta ).uuid;

			// fragment

			data.color = this.color.toJSON( meta ).uuid;
			data.roughness = this.roughness.toJSON( meta ).uuid;
			data.metalness = this.metalness.toJSON( meta ).uuid;

			if ( this.mask ) data.mask = this.mask.toJSON( meta ).uuid;

			if ( this.alpha ) data.alpha = this.alpha.toJSON( meta ).uuid;

			if ( this.normal ) data.normal = this.normal.toJSON( meta ).uuid;

			if ( this.clearcoat ) data.clearcoat = this.clearcoat.toJSON( meta ).uuid;
			if ( this.clearcoatRoughness ) data.clearcoatRoughness = this.clearcoatRoughness.toJSON( meta ).uuid;
			if ( this.clearcoatNormal ) data.clearcoatNormal = this.clearcoatNormal.toJSON( meta ).uuid;

			if ( this.reflectivity ) data.reflectivity = this.reflectivity.toJSON( meta ).uuid;

			if ( this.light ) data.light = this.light.toJSON( meta ).uuid;
			if ( this.shadow ) data.shadow = this.shadow.toJSON( meta ).uuid;

			if ( this.ao ) data.ao = this.ao.toJSON( meta ).uuid;

			if ( this.emissive ) data.emissive = this.emissive.toJSON( meta ).uuid;
			if ( this.ambient ) data.ambient = this.ambient.toJSON( meta ).uuid;

			if ( this.environment ) data.environment = this.environment.toJSON( meta ).uuid;

			if ( this.sheen ) data.sheen = this.sheen.toJSON( meta ).uuid;

		}

		return data;

	}

}

StandardNode.prototype.nodeType = 'Standard';

export { StandardNode };
