import {
	BufferAttribute,
	BufferGeometry,
	Group,
	LineSegments,
	Matrix3,
	Mesh
} from 'three';

import { mergeGeometries } from './BufferGeometryUtils.js';

/**
 * Utility class for LDraw models.
 *
 * @three_import import { LDrawUtils } from 'three/addons/utils/LDrawUtils.js';
 */
class LDrawUtils {

	/**
	 * Merges geometries in the given object by materials and returns a new group object.
	 * Use on not indexed geometries. The object buffers reference the old object ones.
	 * Special treatment is done to the conditional lines generated by LDrawLoader.
	 *
	 * @param {Object3D} object - The object to merge.
	 * @returns {Group} The merged object.
	 */
	static mergeObject( object ) {

		function extractGroup( geometry, group, elementSize, isConditionalLine ) {

			// Extracts a group from a geometry as a new geometry (with attribute buffers referencing original buffers)

			const newGeometry = new BufferGeometry();

			const originalPositions = geometry.getAttribute( 'position' ).array;
			const originalNormals = elementSize === 3 ? geometry.getAttribute( 'normal' ).array : null;

			const numVertsGroup = Math.min( group.count, Math.floor( originalPositions.length / 3 ) - group.start );
			const vertStart = group.start * 3;
			const vertEnd = ( group.start + numVertsGroup ) * 3;

			const positions = originalPositions.subarray( vertStart, vertEnd );
			const normals = originalNormals !== null ? originalNormals.subarray( vertStart, vertEnd ) : null;

			newGeometry.setAttribute( 'position', new BufferAttribute( positions, 3 ) );
			if ( normals !== null ) newGeometry.setAttribute( 'normal', new BufferAttribute( normals, 3 ) );

			if ( isConditionalLine ) {

				const controlArray0 = geometry.getAttribute( 'control0' ).array.subarray( vertStart, vertEnd );
				const controlArray1 = geometry.getAttribute( 'control1' ).array.subarray( vertStart, vertEnd );
				const directionArray = geometry.getAttribute( 'direction' ).array.subarray( vertStart, vertEnd );

				newGeometry.setAttribute( 'control0', new BufferAttribute( controlArray0, 3, false ) );
				newGeometry.setAttribute( 'control1', new BufferAttribute( controlArray1, 3, false ) );
				newGeometry.setAttribute( 'direction', new BufferAttribute( directionArray, 3, false ) );

			}

			return newGeometry;

		}

		function addGeometry( mat, geometry, geometries ) {

			const geoms = geometries[ mat.uuid ];
			if ( ! geoms ) {

				geometries[ mat.uuid ] = {
					mat: mat,
					arr: [ geometry ]
				};

			} else {

				geoms.arr.push( geometry );

			}

		}

		function permuteAttribute( attribute, elemSize ) {

			// Permutes first two vertices of each attribute element

			if ( ! attribute ) return;

			const verts = attribute.array;
			const numVerts = Math.floor( verts.length / 3 );
			let offset = 0;
			for ( let i = 0; i < numVerts; i ++ ) {

				const x = verts[ offset ];
				const y = verts[ offset + 1 ];
				const z = verts[ offset + 2 ];

				verts[ offset ] = verts[ offset + 3 ];
				verts[ offset + 1 ] = verts[ offset + 4 ];
				verts[ offset + 2 ] = verts[ offset + 5 ];

				verts[ offset + 3 ] = x;
				verts[ offset + 4 ] = y;
				verts[ offset + 5 ] = z;

				offset += elemSize * 3;

			}

		}

		// Traverse the object hierarchy collecting geometries and transforming them to world space

		const meshGeometries = {};
		const linesGeometries = {};
		const condLinesGeometries = {};

		object.updateMatrixWorld( true );
		const normalMatrix = new Matrix3();

		object.traverse( c => {

			if ( c.isMesh | c.isLineSegments ) {

				const elemSize = c.isMesh ? 3 : 2;

				const geometry = c.geometry.clone();
				const matrixIsInverted = c.matrixWorld.determinant() < 0;
				if ( matrixIsInverted ) {

					permuteAttribute( geometry.attributes.position, elemSize );
					permuteAttribute( geometry.attributes.normal, elemSize );

				}

				geometry.applyMatrix4( c.matrixWorld );

				if ( c.isConditionalLine ) {

					geometry.attributes.control0.applyMatrix4( c.matrixWorld );
					geometry.attributes.control1.applyMatrix4( c.matrixWorld );
					normalMatrix.getNormalMatrix( c.matrixWorld );
					geometry.attributes.direction.applyNormalMatrix( normalMatrix );

				}

				const geometries = c.isMesh ? meshGeometries : ( c.isConditionalLine ? condLinesGeometries : linesGeometries );

				if ( Array.isArray( c.material ) ) {

					for ( const groupIndex in geometry.groups ) {

						const group = geometry.groups[ groupIndex ];
						const mat = c.material[ group.materialIndex ];
						const newGeometry = extractGroup( geometry, group, elemSize, c.isConditionalLine );
						addGeometry( mat, newGeometry, geometries );

					}

				} else {

					addGeometry( c.material, geometry, geometries );

				}

			}

		} );

		// Create object with merged geometries

		const mergedObject = new Group();

		const meshMaterialsIds = Object.keys( meshGeometries );
		for ( const meshMaterialsId of meshMaterialsIds ) {

			const meshGeometry = meshGeometries[ meshMaterialsId ];
			const mergedGeometry = mergeGeometries( meshGeometry.arr );
			mergedObject.add( new Mesh( mergedGeometry, meshGeometry.mat ) );

		}

		const linesMaterialsIds = Object.keys( linesGeometries );
		for ( const linesMaterialsId of linesMaterialsIds ) {

			const lineGeometry = linesGeometries[ linesMaterialsId ];
			const mergedGeometry = mergeGeometries( lineGeometry.arr );
			mergedObject.add( new LineSegments( mergedGeometry, lineGeometry.mat ) );

		}

		const condLinesMaterialsIds = Object.keys( condLinesGeometries );
		for ( const condLinesMaterialsId of condLinesMaterialsIds ) {

			const condLineGeometry = condLinesGeometries[ condLinesMaterialsId ];
			const mergedGeometry = mergeGeometries( condLineGeometry.arr );
			const condLines = new LineSegments( mergedGeometry, condLineGeometry.mat );
			condLines.isConditionalLine = true;
			mergedObject.add( condLines );

		}

		mergedObject.userData.constructionStep = 0;
		mergedObject.userData.numConstructionSteps = 1;

		return mergedObject;

	}

}

export { LDrawUtils };
