import {
	CubeReflectionMapping,
	CubeRefractionMapping,
	CubeUVReflectionMapping,
	CubeUVRefractionMapping,
	LinearEncoding,
	GammaEncoding
} from 'three';

import { NodeUniform } from './NodeUniform.js';
import { NodeUtils } from './NodeUtils.js';
import { NodeLib } from './NodeLib.js';
import { FunctionNode } from './FunctionNode.js';
import { ConstNode } from './ConstNode.js';
import { StructNode } from './StructNode.js';
import { Vector2Node } from '../inputs/Vector2Node.js';
import { Vector3Node } from '../inputs/Vector3Node.js';
import { Vector4Node } from '../inputs/Vector4Node.js';
import { TextureNode } from '../inputs/TextureNode.js';
import { CubeTextureNode } from '../inputs/CubeTextureNode.js';
import { TextureCubeNode } from '../misc/TextureCubeNode.js';


const elements = NodeUtils.elements,
	constructors = [ 'float', 'vec2', 'vec3', 'vec4' ],
	convertFormatToType = {
		float: 'f',
		vec2: 'v2',
		vec3: 'v3',
		vec4: 'v4',
		mat4: 'v4',
		int: 'i',
		bool: 'b'
	},
	convertTypeToFormat = {
		t: 'sampler2D',
		tc: 'samplerCube',
		b: 'bool',
		i: 'int',
		f: 'float',
		c: 'vec3',
		v2: 'vec2',
		v3: 'vec3',
		v4: 'vec4',
		m3: 'mat3',
		m4: 'mat4'
	};

class NodeBuilder {

	constructor() {

		this.slots = [];
		this.caches = [];
		this.contexts = [];

		this.keywords = {};

		this.nodeData = {};

		this.requires = {
			uv: [],
			color: [],
			lights: false,
			fog: false,
			transparent: false,
			irradiance: false
		};

		this.includes = {
			consts: [],
			functions: [],
			structs: []
		};

		this.attributes = {};

		this.prefixCode = /* glsl */`
			#ifdef TEXTURE_LOD_EXT

				#define texCube(a, b) textureCube(a, b)
				#define texCubeBias(a, b, c) textureCubeLodEXT(a, b, c)

				#define tex2D(a, b) texture2D(a, b)
				#define tex2DBias(a, b, c) texture2DLodEXT(a, b, c)

			#else

				#define texCube(a, b) textureCube(a, b)
				#define texCubeBias(a, b, c) textureCube(a, b, c)

				#define tex2D(a, b) texture2D(a, b)
				#define tex2DBias(a, b, c) texture2D(a, b, c)

			#endif

			#include <packing>
			#include <common>`;

		this.parsCode = {
			vertex: '',
			fragment: ''
		};

		this.code = {
			vertex: '',
			fragment: ''
		};

		this.nodeCode = {
			vertex: '',
			fragment: ''
		};

		this.resultCode = {
			vertex: '',
			fragment: ''
		};

		this.finalCode = {
			vertex: '',
			fragment: ''
		};

		this.inputs = {
			uniforms: {
				list: [],
				vertex: [],
				fragment: []
			},
			vars: {
				varying: [],
				vertex: [],
				fragment: []
			}
		};

		// send to material

		this.defines = {};

		this.uniforms = {};

		this.extensions = {};

		this.updaters = [];

		this.nodes = [];

		// --

		this.analyzing = false;

	}

	build( vertex, fragment ) {

		this.buildShader( 'vertex', vertex );
		this.buildShader( 'fragment', fragment );

		for ( let i = 0; i < this.requires.uv.length; i ++ ) {

			if ( this.requires.uv[ i ] ) {

				const uvIndex = i > 0 ? i + 1 : '';

				this.addVaryCode( 'varying vec2 vUv' + uvIndex + ';' );

				if ( i > 0 ) {

					this.addVertexParsCode( 'attribute vec2 uv' + uvIndex + ';' );

				}

				this.addVertexFinalCode( 'vUv' + uvIndex + ' = uv' + uvIndex + ';' );

			}

		}

		if ( this.requires.color[ 0 ] ) {

			this.addVaryCode( 'varying vec4 vColor;' );
			this.addVertexParsCode( 'attribute vec4 color;' );

			this.addVertexFinalCode( 'vColor = color;' );

		}

		if ( this.requires.color[ 1 ] ) {

			this.addVaryCode( 'varying vec4 vColor2;' );
			this.addVertexParsCode( 'attribute vec4 color2;' );

			this.addVertexFinalCode( 'vColor2 = color2;' );

		}

		if ( this.requires.position ) {

			this.addVaryCode( 'varying vec3 vPosition;' );

			this.addVertexFinalCode( 'vPosition = transformed;' );

		}

		if ( this.requires.worldPosition ) {

			this.addVaryCode( 'varying vec3 vWPosition;' );

			this.addVertexFinalCode( 'vWPosition = ( modelMatrix * vec4( transformed, 1.0 ) ).xyz;' );

		}

		if ( this.requires.normal ) {

			this.addVaryCode( 'varying vec3 vObjectNormal;' );

			this.addVertexFinalCode( 'vObjectNormal = normal;' );

		}

		if ( this.requires.worldNormal ) {

			this.addVaryCode( 'varying vec3 vWNormal;' );

			this.addVertexFinalCode( 'vWNormal = inverseTransformDirection( transformedNormal, viewMatrix ).xyz;' );

		}

		return this;

	}

	buildShader( shader, node ) {

		this.resultCode[ shader ] = node.build( this.setShader( shader ), 'v4' );

	}

	setMaterial( material, renderer ) {

		this.material = material;
		this.renderer = renderer;

		this.requires.lights = material.lights;
		this.requires.fog = material.fog;

		this.mergeDefines( material.defines );

		return this;

	}

	addFlow( slot, cache, context ) {

		return this.addSlot( slot ).addCache( cache ).addContext( context );

	}

	removeFlow() {

		return this.removeSlot().removeCache().removeContext();

	}

	addCache( name ) {

		this.cache = name || '';
		this.caches.push( this.cache );

		return this;

	}

	removeCache() {

		this.caches.pop();
		this.cache = this.caches[ this.caches.length - 1 ] || '';

		return this;

	}

	addContext( context ) {

		this.context = Object.assign( {}, this.context, context );
		this.context.extra = this.context.extra || {};

		this.contexts.push( this.context );

		return this;

	}

	removeContext() {

		this.contexts.pop();
		this.context = this.contexts[ this.contexts.length - 1 ] || {};

		return this;

	}

	addSlot( name = '' ) {

		this.slot = name;
		this.slots.push( this.slot );

		return this;

	}

	removeSlot() {

		this.slots.pop();
		this.slot = this.slots[ this.slots.length - 1 ] || '';

		return this;

	}

	addVertexCode( code ) {

		this.addCode( code, 'vertex' );

	}

	addFragmentCode( code ) {

		this.addCode( code, 'fragment' );

	}

	addCode( code, shader ) {

		this.code[ shader || this.shader ] += code + '\n';

	}

	addVertexNodeCode( code ) {

		this.addNodeCode( code, 'vertex' );

	}

	addFragmentNodeCode( code ) {

		this.addNodeCode( code, 'fragment' );

	}

	addNodeCode( code, shader ) {

		this.nodeCode[ shader || this.shader ] += code + '\n';

	}

	clearNodeCode( shader ) {

		shader = shader || this.shader;

		const code = this.nodeCode[ shader ];

		this.nodeCode[ shader ] = '';

		return code;

	}

	clearVertexNodeCode( ) {

		return this.clearNodeCode( 'vertex' );

	}

	clearFragmentNodeCode( ) {

		return this.clearNodeCode( 'fragment' );

	}

	addVertexFinalCode( code ) {

		this.addFinalCode( code, 'vertex' );

	}

	addFragmentFinalCode( code ) {

		this.addFinalCode( code, 'fragment' );

	}

	addFinalCode( code, shader ) {

		this.finalCode[ shader || this.shader ] += code + '\n';

	}

	addVertexParsCode( code ) {

		this.addParsCode( code, 'vertex' );

	}

	addFragmentParsCode( code ) {

		this.addParsCode( code, 'fragment' );

	}

	addParsCode( code, shader ) {

		this.parsCode[ shader || this.shader ] += code + '\n';

	}

	addVaryCode( code ) {

		this.addVertexParsCode( code );
		this.addFragmentParsCode( code );

	}

	isCache( name ) {

		return this.caches.indexOf( name ) !== - 1;

	}

	isSlot( name ) {

		return this.slots.indexOf( name ) !== - 1;

	}

	define( name, value ) {

		this.defines[ name ] = value === undefined ? 1 : value;

	}

	require( name ) {

		this.requires[ name ] = true;

	}

	isDefined( name ) {

		return this.defines[ name ] !== undefined;

	}

	getVar( uuid, type, ns, shader = 'varying', prefix = 'V', label = '' ) {

		const vars = this.getVars( shader );
		let data = vars[ uuid ];

		if ( ! data ) {

			const index = vars.length,
				name = ns ? ns : 'node' + prefix + index + ( label ? '_' + label : '' );

			data = { name: name, type: type };

			vars.push( data );
			vars[ uuid ] = data;

		}

		return data;

	}

	getTempVar( uuid, type, ns, label ) {

		return this.getVar( uuid, type, ns, this.shader, 'T', label );

	}

	getAttribute( name, type ) {

		if ( ! this.attributes[ name ] ) {

			const varying = this.getVar( name, type );

			this.addVertexParsCode( 'attribute ' + type + ' ' + name + ';' );
			this.addVertexFinalCode( varying.name + ' = ' + name + ';' );

			this.attributes[ name ] = { varying: varying, name: name, type: type };

		}

		return this.attributes[ name ];

	}

	getCode( shader ) {

		return [
			this.prefixCode,
			this.parsCode[ shader ],
			this.getVarListCode( this.getVars( 'varying' ), 'varying' ),
			this.getVarListCode( this.inputs.uniforms[ shader ], 'uniform' ),
			this.getIncludesCode( 'consts', shader ),
			this.getIncludesCode( 'structs', shader ),
			this.getIncludesCode( 'functions', shader ),
			'void main() {',
			this.getVarListCode( this.getVars( shader ) ),
			this.code[ shader ],
			this.resultCode[ shader ],
			this.finalCode[ shader ],
			'}'
		].join( '\n' );

	}

	getVarListCode( vars, prefix = '' ) {

		let code = '';

		for ( let i = 0, l = vars.length; i < l; ++ i ) {

			const nVar = vars[ i ],
				type = nVar.type,
				name = nVar.name;

			const formatType = this.getFormatByType( type );

			if ( formatType === undefined ) {

				throw new Error( 'Node pars ' + formatType + ' not found.' );

			}

			code += prefix + ' ' + formatType + ' ' + name + ';\n';

		}

		return code;

	}

	getVars( shader ) {

		return this.inputs.vars[ shader || this.shader ];

	}

	getNodeData( node ) {

		const uuid = node.isNode ? node.uuid : node;

		return this.nodeData[ uuid ] = this.nodeData[ uuid ] || {};

	}

	createUniform( shader, type, node, ns, needsUpdate, label ) {

		const uniforms = this.inputs.uniforms,
			index = uniforms.list.length;

		const uniform = new NodeUniform( {
			type: type,
			name: ns ? ns : 'nodeU' + index + ( label ? '_' + label : '' ),
			node: node,
			needsUpdate: needsUpdate
		} );

		uniforms.list.push( uniform );

		uniforms[ shader ].push( uniform );
		uniforms[ shader ][ uniform.name ] = uniform;

		this.uniforms[ uniform.name ] = uniform;

		return uniform;

	}

	createVertexUniform( type, node, ns, needsUpdate, label ) {

		return this.createUniform( 'vertex', type, node, ns, needsUpdate, label );

	}

	createFragmentUniform( type, node, ns, needsUpdate, label ) {

		return this.createUniform( 'fragment', type, node, ns, needsUpdate, label );

	}

	include( node, parent, source ) {

		let includesStruct;

		node = typeof node === 'string' ? NodeLib.get( node ) : node;

		if ( this.context.include === false ) {

			return node.name;

		}


		if ( node instanceof FunctionNode ) {

			includesStruct = this.includes.functions;

		} else if ( node instanceof ConstNode ) {

			includesStruct = this.includes.consts;

		} else if ( node instanceof StructNode ) {

			includesStruct = this.includes.structs;

		}

		const includes = includesStruct[ this.shader ] = includesStruct[ this.shader ] || [];

		if ( node ) {

			let included = includes[ node.name ];

			if ( ! included ) {

				included = includes[ node.name ] = {
					node: node,
					deps: []
				};

				includes.push( included );

				included.src = node.build( this, 'source' );

			}

			if ( node instanceof FunctionNode && parent && includes[ parent.name ] && includes[ parent.name ].deps.indexOf( node ) == - 1 ) {

				includes[ parent.name ].deps.push( node );

				if ( node.includes && node.includes.length ) {

					let i = 0;

					do {

						this.include( node.includes[ i ++ ], parent );

					} while ( i < node.includes.length );

				}

			}

			if ( source ) {

				included.src = source;

			}

			return node.name;

		} else {

			throw new Error( 'Include not found.' );

		}

	}

	colorToVectorProperties( color ) {

		return color.replace( 'r', 'x' ).replace( 'g', 'y' ).replace( 'b', 'z' ).replace( 'a', 'w' );

	}

	colorToVector( color ) {

		return color.replace( /c/g, 'v3' );

	}

	getIncludes( type, shader ) {

		return this.includes[ type ][ shader || this.shader ];

	}

	getIncludesCode( type, shader ) {

		let includes = this.getIncludes( type, shader );

		if ( ! includes ) return '';

		let code = '';

		includes = includes.sort( sortByPosition );

		for ( let i = 0; i < includes.length; i ++ ) {

			if ( includes[ i ].src ) code += includes[ i ].src + '\n';

		}

		return code;

	}

	getConstructorFromLength( len ) {

		return constructors[ len - 1 ];

	}

	isTypeMatrix( format ) {

		return /^m/.test( format );

	}

	getTypeLength( type ) {

		if ( type === 'f' ) return 1;

		return parseInt( this.colorToVector( type ).substr( 1 ) );

	}

	getTypeFromLength( len ) {

		if ( len === 1 ) return 'f';

		return 'v' + len;

	}

	findNode() {

		for ( let i = 0; i < arguments.length; i ++ ) {

			const nodeCandidate = arguments[ i ];

			if ( nodeCandidate !== undefined && nodeCandidate.isNode ) {

				return nodeCandidate;

			}

		}

	}

	resolve() {

		for ( let i = 0; i < arguments.length; i ++ ) {

			const nodeCandidate = arguments[ i ];

			if ( nodeCandidate !== undefined ) {

				if ( nodeCandidate.isNode ) {

					return nodeCandidate;

				} else if ( nodeCandidate.isTexture ) {

					switch ( nodeCandidate.mapping ) {

						case CubeReflectionMapping:
						case CubeRefractionMapping:

							return new CubeTextureNode( nodeCandidate );

							break;

						case CubeUVReflectionMapping:
						case CubeUVRefractionMapping:

							return new TextureCubeNode( new TextureNode( nodeCandidate ) );

							break;

						default:

							return new TextureNode( nodeCandidate );

					}

				} else if ( nodeCandidate.isVector2 ) {

					return new Vector2Node( nodeCandidate );

				} else if ( nodeCandidate.isVector3 ) {

					return new Vector3Node( nodeCandidate );

				} else if ( nodeCandidate.isVector4 ) {

					return new Vector4Node( nodeCandidate );

				}

			}

		}

	}

	format( code, from, to ) {

		const typeToType = this.colorToVector( to + ' <- ' + from );

		switch ( typeToType ) {

			case 'f <- v2' : return code + '.x';
			case 'f <- v3' : return code + '.x';
			case 'f <- v4' : return code + '.x';
			case 'f <- i' :
			case 'f <- b' :	return 'float( ' + code + ' )';

			case 'v2 <- f' : return 'vec2( ' + code + ' )';
			case 'v2 <- v3': return code + '.xy';
			case 'v2 <- v4': return code + '.xy';
			case 'v2 <- i' :
			case 'v2 <- b' : return 'vec2( float( ' + code + ' ) )';

			case 'v3 <- f' : return 'vec3( ' + code + ' )';
			case 'v3 <- v2': return 'vec3( ' + code + ', 0.0 )';
			case 'v3 <- v4': return code + '.xyz';
			case 'v3 <- i' :
			case 'v3 <- b' : return 'vec2( float( ' + code + ' ) )';

			case 'v4 <- f' : return 'vec4( ' + code + ' )';
			case 'v4 <- v2': return 'vec4( ' + code + ', 0.0, 1.0 )';
			case 'v4 <- v3': return 'vec4( ' + code + ', 1.0 )';
			case 'v4 <- i' :
			case 'v4 <- b' : return 'vec4( float( ' + code + ' ) )';

			case 'i <- f' :
			case 'i <- b' : return 'int( ' + code + ' )';
			case 'i <- v2' : return 'int( ' + code + '.x )';
			case 'i <- v3' : return 'int( ' + code + '.x )';
			case 'i <- v4' : return 'int( ' + code + '.x )';

			case 'b <- f' : return '( ' + code + ' != 0.0 )';
			case 'b <- v2' : return '( ' + code + ' != vec2( 0.0 ) )';
			case 'b <- v3' : return '( ' + code + ' != vec3( 0.0 ) )';
			case 'b <- v4' : return '( ' + code + ' != vec4( 0.0 ) )';
			case 'b <- i' : return '( ' + code + ' != 0 )';

		}

		return code;

	}

	getTypeByFormat( format ) {

		return convertFormatToType[ format ] || format;

	}

	getFormatByType( type ) {

		return convertTypeToFormat[ type ] || type;

	}

	getUuid( uuid, useCache ) {

		useCache = useCache !== undefined ? useCache : true;

		if ( useCache && this.cache ) uuid = this.cache + '-' + uuid;

		return uuid;

	}

	getElementByIndex( index ) {

		return elements[ index ];

	}

	getIndexByElement( elm ) {

		return elements.indexOf( elm );

	}

	isShader( shader ) {

		return this.shader === shader;

	}

	setShader( shader ) {

		this.shader = shader;

		return this;

	}

	mergeDefines( defines ) {

		for ( const name in defines ) {

			this.defines[ name ] = defines[ name ];

		}

		return this.defines;

	}

	mergeUniform( uniforms ) {

		for ( const name in uniforms ) {

			this.uniforms[ name ] = uniforms[ name ];

		}

		return this.uniforms;

	}

	getTextureEncodingFromMap( map ) {

		let encoding;

		if ( ! map ) {

			encoding = LinearEncoding;

		} else if ( map.isTexture ) {

			encoding = map.encoding;

		} else if ( map.isWebGLRenderTarget ) {

			console.warn( 'THREE.WebGLPrograms.getTextureEncodingFromMap: don\'t use render targets as textures. Use their .texture property instead.' );
			encoding = map.texture.encoding;

		}

		if ( encoding === LinearEncoding && this.context.gamma ) {

			encoding = GammaEncoding;

		}

		return encoding;

	}

}

function sortByPosition( a, b ) {

	return a.deps.length - b.deps.length;

}

export { NodeBuilder };
