( function () {

	const _A = new THREE.Vector3();

	const _B = new THREE.Vector3();

	const _C = new THREE.Vector3();

	class EdgeSplitModifier {

		modify( geometry, cutOffAngle, tryKeepNormals = true ) {

			function computeNormals() {

				normals = new Float32Array( indexes.length * 3 );

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

					let index = indexes[ i ];

					_A.set( positions[ 3 * index ], positions[ 3 * index + 1 ], positions[ 3 * index + 2 ] );

					index = indexes[ i + 1 ];

					_B.set( positions[ 3 * index ], positions[ 3 * index + 1 ], positions[ 3 * index + 2 ] );

					index = indexes[ i + 2 ];

					_C.set( positions[ 3 * index ], positions[ 3 * index + 1 ], positions[ 3 * index + 2 ] );

					_C.sub( _B );

					_A.sub( _B );

					const normal = _C.cross( _A ).normalize();

					for ( let j = 0; j < 3; j ++ ) {

						normals[ 3 * ( i + j ) ] = normal.x;
						normals[ 3 * ( i + j ) + 1 ] = normal.y;
						normals[ 3 * ( i + j ) + 2 ] = normal.z;

					}

				}

			}

			function mapPositionsToIndexes() {

				pointToIndexMap = Array( positions.length / 3 );

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

					const index = indexes[ i ];

					if ( pointToIndexMap[ index ] == null ) {

						pointToIndexMap[ index ] = [];

					}

					pointToIndexMap[ index ].push( i );

				}

			}

			function edgeSplitToGroups( indexes, cutOff, firstIndex ) {

				_A.set( normals[ 3 * firstIndex ], normals[ 3 * firstIndex + 1 ], normals[ 3 * firstIndex + 2 ] ).normalize();

				const result = {
					splitGroup: [],
					currentGroup: [ firstIndex ]
				};

				for ( const j of indexes ) {

					if ( j !== firstIndex ) {

						_B.set( normals[ 3 * j ], normals[ 3 * j + 1 ], normals[ 3 * j + 2 ] ).normalize();

						if ( _B.dot( _A ) < cutOff ) {

							result.splitGroup.push( j );

						} else {

							result.currentGroup.push( j );

						}

					}

				}

				return result;

			}

			function edgeSplit( indexes, cutOff, original = null ) {

				if ( indexes.length === 0 ) return;
				const groupResults = [];

				for ( const index of indexes ) {

					groupResults.push( edgeSplitToGroups( indexes, cutOff, index ) );

				}

				let result = groupResults[ 0 ];

				for ( const groupResult of groupResults ) {

					if ( groupResult.currentGroup.length > result.currentGroup.length ) {

						result = groupResult;

					}

				}

				if ( original != null ) {

					splitIndexes.push( {
						original: original,
						indexes: result.currentGroup
					} );

				}

				if ( result.splitGroup.length ) {

					edgeSplit( result.splitGroup, cutOff, original || result.currentGroup[ 0 ] );

				}

			}

			if ( geometry.isGeometry === true ) {

				console.error( 'THREE.EdgeSplitModifier no longer supports THREE.Geometry. Use THREE.BufferGeometry instead.' );
				return;

			}

			let hadNormals = false;
			let oldNormals = null;

			if ( geometry.attributes.normal ) {

				hadNormals = true;
				geometry = geometry.clone();

				if ( tryKeepNormals === true && geometry.index !== null ) {

					oldNormals = geometry.attributes.normal.array;

				}

				geometry.deleteAttribute( 'normal' );

			}

			if ( geometry.index == null ) {

				if ( THREE.BufferGeometryUtils === undefined ) {

					throw 'THREE.EdgeSplitModifier relies on THREE.BufferGeometryUtils';

				}

				geometry = THREE.BufferGeometryUtils.mergeVertices( geometry );

			}

			const indexes = geometry.index.array;
			const positions = geometry.getAttribute( 'position' ).array;
			let normals;
			let pointToIndexMap;
			computeNormals();
			mapPositionsToIndexes();
			const splitIndexes = [];

			for ( const vertexIndexes of pointToIndexMap ) {

				edgeSplit( vertexIndexes, Math.cos( cutOffAngle ) - 0.001 );

			}

			const newAttributes = {};

			for ( const name of Object.keys( geometry.attributes ) ) {

				const oldAttribute = geometry.attributes[ name ];
				const newArray = new oldAttribute.array.constructor( ( indexes.length + splitIndexes.length ) * oldAttribute.itemSize );
				newArray.set( oldAttribute.array );
				newAttributes[ name ] = new THREE.BufferAttribute( newArray, oldAttribute.itemSize, oldAttribute.normalized );

			}

			const newIndexes = new Uint32Array( indexes.length );
			newIndexes.set( indexes );

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

				const split = splitIndexes[ i ];
				const index = indexes[ split.original ];

				for ( const attribute of Object.values( newAttributes ) ) {

					for ( let j = 0; j < attribute.itemSize; j ++ ) {

						attribute.array[ ( indexes.length + i ) * attribute.itemSize + j ] = attribute.array[ index * attribute.itemSize + j ];

					}

				}

				for ( const j of split.indexes ) {

					newIndexes[ j ] = indexes.length + i;

				}

			}

			geometry = new THREE.BufferGeometry();
			geometry.setIndex( new THREE.BufferAttribute( newIndexes, 1 ) );

			for ( const name of Object.keys( newAttributes ) ) {

				geometry.setAttribute( name, newAttributes[ name ] );

			}

			if ( hadNormals ) {

				geometry.computeVertexNormals();

				if ( oldNormals !== null ) {

					const changedNormals = new Array( oldNormals.length / 3 ).fill( false );

					for ( const splitData of splitIndexes ) changedNormals[ splitData.original ] = true;

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

						if ( changedNormals[ i ] === false ) {

							for ( let j = 0; j < 3; j ++ ) geometry.attributes.normal.array[ 3 * i + j ] = oldNormals[ 3 * i + j ];

						}

					}

				}

			}

			return geometry;

		}

	}

	THREE.EdgeSplitModifier = EdgeSplitModifier;

} )();
