import {
	UniformsLib,
	UniformsUtils
} from '../../../../../build/three.module.js';

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';

function StandardNode() {

	Node.call( this );

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

}

StandardNode.prototype = Object.create( Node.prototype );
StandardNode.prototype.constructor = StandardNode;
StandardNode.prototype.nodeType = 'Standard';

StandardNode.prototype.build = function ( builder ) {

	var code;

	builder.define( 'STANDARD' );

	var 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' ) ) {

		var 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( [
			'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>'

		].join( '\n' ) );

		var 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 {

		var specularRoughness = new ExpressionNode( 'material.specularRoughness', 'f' );
		var clearcoatRoughness = new ExpressionNode( 'material.clearcoatRoughness', 'f' );

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

		var contextGammaOnly = {
			gamma: true
		};

		var contextClearcoatEnvironment = {
			roughness: clearcoatRoughness,
			bias: new SpecularMIPLevelNode( clearcoatRoughness ),
			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

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

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

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

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

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

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

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

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

		var 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' } );

			}

		}

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

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

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

		builder.addParsCode( [
			'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>'
		].join( '\n' ) );

		var 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;

};

StandardNode.prototype.copy = function ( source ) {

	Node.prototype.copy.call( this, 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;

};

StandardNode.prototype.toJSON = function ( meta ) {

	var 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;

};

export { StandardNode };
