Source: package/mesh/generated_mesh.js

import { Reporter } from '../../reporter/reporter.js';
import { WrappedTexture } from '../wrapped_texture.js';
import { WrappedMaterial } from '../material/wrapped_material.js';
import { checkPropTypes } from '../../lib.js';
import { BuildableComponent } from '../component/buildable_component.js';
import { Component } from '../../component/component.js';
//import { Geometry } from '../geometry/geometry.js';
import { MaterialSet } from '../material/material_set.js';
import { Project } from '../../project.js';
//import { GLTF } from './gltf.js';


import { ObjectSpaceNormalMap, Vector2, Vector3, DoubleSide, LinearSRGBColorSpace, TextureLoader, RepeatWrapping, 
	BoxGeometry, 
	CylinderGeometry, 
	SphereGeometry, 
	Mesh, 
	Shape,  
	ExtrudeGeometry, 
	CurvePath,
	LineCurve3
} from '../../../node_modules/three/build/three.module.js';


import { buildNodeMaterial } from '../../utilities/nodeMaterialBuilder.js';
import { buildProfiledContour } from '../../utilities/profiledContourBuilder.js';
import { applyBoxUV } from '../../utilities/applyBoxMapping.js';

/** 
 * ProductBuilder Generated Mesh (in development)
 * 
 */

// Info met profiled contour geometry
// https://discourse.threejs.org/t/profiledcontourgeometry-multimaterial/5801/10

// Info met bevel
// https://threejs.org/docs/#api/en/geometries/ExtrudeGeometry

// Lathe
// Extrude + Bevel

class GeneratedMesh extends BuildableComponent {

	constructor( reporter, settings ) {

        super(
            reporter,
            settings
        );

		checkPropTypes(
            settings,
            {
                meshName: 'string',
				shape: 'string',   //box, circle, cone, cylinder, sphere
                defaultMaterial: WrappedMaterial,
            },
            {
                materialVariantGroup: MaterialSet,
                tilingMultiplier: 'number',
                aoMap: WrappedTexture,		
                aoMapIntensity: 'number',
                normalMap: WrappedTexture,
                normalMapIntensity: 'number',
				shape: 'string',
				dimensions: ''
            }
		);

		// fucking typescript
		if (!Object.getOwnPropertyDescriptor(this,'materialVariantGroup')) {
			/** @type {MaterialSet} */
			this.materialVariantGroup = settings.materialVariantGroup;
		}

		// Map the pkg json settings onto the instantiated class
		this._tilingMultiplier = settings.tilingMultiplier
		this._aoMap = settings.aoMap
		this._aoMapIntensity = settings.aoMapIntensity
		this._normalMap = settings.normalMap ///-> dit is dubbel
		this._normalMapTiling = settings.normalMapTiling
		this._normalMapScale = settings.normalMapIntensity ///-> dit is dubbel
		this._meshObject = null;

	}    

	static _exportName = {
		singular: 'generatedMesh',
		plural: 'generatedMeshes'
	};

	/**
	 * @param {WrappedMaterial} material 
	 */

	async buildMeshMaterial( material ){
	
		//get the product normal map
		const normalMap = this._normalMap.content.main.medium
		normalMap.colorSpace = LinearSRGBColorSpace
		//normalMap.flipY = true; //deze is (nog) niet nodig
		normalMap.wrapS = RepeatWrapping;
		normalMap.wrapT = RepeatWrapping;
		normalMap.repeat = this._normalMapTiling
		
		const normalMapTiling = this._normalMapTiling

		//create texture node with the product normal map
		// const normalNode 	= normalMap
		//                     ?	new Nodes.TextureNode( normalMap )
		//                     :	undefined

		const normalNode = normalMap

		//create the material
		const nodeMaterial = buildNodeMaterial(
			material,   
			normalNode,	
			normalMapTiling
		)

		//copy the id from the currentmaterial
		nodeMaterial.userData = { PB: { origin: material.userData.PB.origin } }

		//add the environment map
		nodeMaterial.envMap = material.envMap

		this._meshObject.mesh.material = nodeMaterial
		this._meshObject.mesh.material.normalScale = { x:2, y:2 }
		this._meshObject.mesh.material.needsUpdate = true;

		return nodeMaterial

	}

	async _build(part, quality, dependencies) {

		this._status[ part ][ quality ].setState( 'loading');


		//Cylinder with 3 materials for the side!
		//https://stackoverflow.com/questions/8315546/texturing-a-cylinder-in-three-js
		// const materials = [
		// 	sideMaterial,
		// 	topMaterial,
		// 	bottomMaterial
		//   ]
		//   const geometry = new THREE.CylinderGeometry(5, 5, 0.5, 100)
		//   const mesh= new THREE.Mesh(geometry, materials)

		switch (part) {

			case 'UI':

				// Get the thumbnail from the pkg json settings
				if (this._settings.thumbnail) {
					this._setContent('UI', quality, this._settings.thumbnail.content.main[quality]);
				}
				else {
					this._setContent('UI', quality, Project.defaultImages.missing.cloneNode(true));
				}

			break;

			case 'main':

				//console.log( dependencies )

				// Get the materialVariantGroup from the pkg json settings
				if ( this._settings.materialVariantGroup && ! this._settings.materialVariantGroup.has( this._settings.defaultMaterial )){
					// console.log( settings.materialVariantGroup )
					//throw new Error( `Can not construct ${this.label} because def mat ${this._settings.defaultMaterial.name || this._settings.defaultMaterial.id} is not in its matVarGrp ${settings.materialVariantGroup.name || settings.materialVariantGroup.id}` )
				}

				// Create the primitive

				let mesh, geometry, uvs;
				
				switch( this.shape ){

					case "box": 

						console.log("generate box");
						const width = 2;
						const height = 2;
						const depth = 1;
						geometry = new BoxGeometry(width, height, depth);

						uvs = geometry.attributes.uv.array;

						// Right and Left faces (width x height)
						// Right face UVs (first 4 pairs)
						for (var i = 0; i < 8; i += 2) {
							uvs[i] *= depth;  // Scale U (x axis) for width
							uvs[i + 1] *= height;  // Scale V (y axis) for height
						}

						// Left face UVs (next 4 pairs)
						for (var i = 8; i < 16; i += 2) {
							uvs[i] *= depth;  // Scale U (x axis) for width
							uvs[i + 1] *= height;  // Scale V (y axis) for height
						}

						// Top and Bottom faces (width x depth)
						// Top face UVs (next 4 pairs)
						for (var i = 16; i < 24; i += 2) {
							uvs[i] *= width;  // Scale U (x axis) for width
							uvs[i + 1] *= depth;  // Scale V (y axis) for depth
						}

						// Bottom face UVs (next 4 pairs)
						for (var i = 24; i < 32; i += 2) {
							uvs[i] *= width;  // Scale U (x axis) for width
							uvs[i + 1] *= depth;  // Scale V (y axis) for depth
						}

						// Front and Back faces (depth x height)
						// Front face UVs (next 4 pairs)
						for (var i = 32; i < 40; i += 2) {
							uvs[i] *= width;  // Scale U (x axis) for depth
							uvs[i + 1] *= height;  // Scale V (y axis) for height
						}

						// Back face UVs (last 4 pairs)
						for (var i = 40; i < 48; i += 2) {
							uvs[i] *= width;  // Scale U (x axis) for depth
							uvs[i + 1] *= height;  // Scale V (y axis) for height
						}

						// Flag UVs for update
						geometry.attributes.uv.needsUpdate = true;

					break;

					case "cylinder":

						console.log("generate cylinder");

						const cylinderDiameter = 2

						const cylinderRadius = cylinderDiameter / 2  
						const radiusTop = cylinderRadius ;
						const radiusBottom = cylinderRadius ;
						const cylinderHeight = 0.05;

						// The amount of segments on the side of the cylinder
						const radialSegments = 16;
						const heightSegments = 1

						// The amount of tiling on the sides of the cyylinder.
						const sideTilingX = 1;
						const sideTilingY = 1

						geometry = new CylinderGeometry(radiusTop, radiusBottom, cylinderHeight, radialSegments, heightSegments);

						const uvs = geometry.attributes.uv.array;
						const radialSegmentCount = radialSegments - 1; // +1 for wrapping

						// Calculate sideUVCount to handle UVs for the sides
						const sideUVCount = (radialSegments + 1) * 2 * (heightSegments + 1) ; // Sides UVs count
						//console.log( "sideUVCount:", sideUVCount)

						// Adjust UVs for the seam on the side of the cylinder
						for (let i = 0; i < sideUVCount; i += 2) {
							let u = uvs[i];
							let v = uvs[i + 1];

							// Check for seam condition: if this vertex is at the last radial segment
							const segmentIndex = Math.floor(i / 2) % radialSegmentCount;
							if (segmentIndex === radialSegments) {
								u = 0; // Force U to wrap around for the last vertex
							}

							// Scale UVs for demonstration (adjust to your needs)
							uvs[i] = u * sideTilingX; // Scale U (horizontal) by 2
							uvs[i + 1] = v * sideTilingY; // Scale V (vertical) by 2
						}

						// Top and Bottom caps:
						const topCapStart = sideUVCount; // Start of top cap UVs
						let bottomCapStart = topCapStart + radialSegments * 3 * 2 ; // Start of bottom cap UVs

						let bottomCapStart2 = bottomCapStart - ( radialSegments * 2 - 2 );

						// Top cap UVs (scales by radiusTop)
						for ( let i = topCapStart; i < bottomCapStart2 ; i += 2 ) {
							// Scale U and V for the top cap based on radiusTop
							uvs[i] *= cylinderDiameter //cylinderRadius //1;  // Scale U for top cap (can apply custom scaling)
							uvs[i + 1] *= cylinderDiameter // cylinderRadius // 1;  // Scale V for top cap
						}

						// Bottom cap UVs (scales by radiusBottom)
						for ( let i = bottomCapStart2  ; i < uvs.length ; i += 2 ) { // // length2
							// Scale U and V for the bottom cap based on radiusBottom
							uvs[i] *= cylinderDiameter;      // Scale U (horizontal) by radiusBottom (can apply custom scaling)
							uvs[i + 1] *= cylinderDiameter;  // Scale V (vertical) by radiusBottom
						}

						// Flag UVs for update
						geometry.attributes.uv.needsUpdate = true;

					break;



					case "sphere":
						console.log( "generate sphere");
						const radius = 2;
						const widthSegments = 16;
						const heightSegments2 = 16;
						geometry = new SphereGeometry( radius, widthSegments, heightSegments2 );
					break;


					case "contour":

						console.log("generate contour");

						const shape = new Shape();

						const type = "rectangle";

						const contourWidth = 2
						const contourDepth = 1
						let contourHeight = 0.05

						const bevelEnabled = false;
						const bevelSize = 0.01;

						switch(type){
							case "rectangle":
								// Define the corners of the square
								const size = 1; // Length of each side of the square

								// Move to the starting point (bottom-left corner)
								shape.moveTo( 0, 0 );

								// Draw lines to each corner of the square
								shape.lineTo( contourWidth, 0 );   // Bottom-right corner
								shape.lineTo( contourWidth, contourDepth ); // Top-right corner
								shape.lineTo( 0, contourDepth );   // Top-left corner
								shape.lineTo( 0, 0 );      // Back to bottom-left corner (close the shape)

							break;
						}

						if( bevelEnabled ){
							contourHeight = contourHeight - 2 * bevelSize
						}

						const extrudeSettings = { 
							depth: contourHeight, 
							bevelEnabled: bevelEnabled, 
							bevelSegments: 2, 
							steps: 2, 
							bevelSize: 0.01, 
							bevelThickness: 0.01
						};

						geometry = new ExtrudeGeometry( shape, extrudeSettings );

						// Rotate the geometry to the XZ plane
						geometry.rotateX(-Math.PI / 2);

					break;

					case "profile":
						// Define the contour (path) for sweeping
						const contourPath = new CurvePath();

						// Create a path that matches your rectangle shape (XZ plane)
						const profileWidth = 2;
						const profileDepth = 1;

						const contour = new Shape();
						contour.moveTo(0, 0);
						contour.lineTo(profileWidth, 0);
						contour.lineTo(profileWidth, profileDepth);
						contour.lineTo(0, profileDepth);
						contour.lineTo(0, 0);

						// Convert the shape into a path for sweeping
						contourPath.add(new LineCurve3(
							new Vector3(0, 0, 0),
							new Vector3(profileWidth, 0, 0)
						));
						contourPath.add(new LineCurve3(
							new Vector3(profileWidth, 0, 0),
							new Vector3(profileWidth, 0, profileDepth)
						));
						contourPath.add(new LineCurve3(
							new Vector3(profileWidth, 0, profileDepth),
							new Vector3(0, 0, profileDepth)
						));
						contourPath.add(new LineCurve3(
							new Vector3(0, 0, profileDepth),
							new Vector3(0, 0, 0)
						));

						// Define the profile shape (circle)
						const profileShape = new Shape();
						const radius1 = 0.025; // Radius of the profile (circle)
						profileShape.moveTo(radius1, 0);
						profileShape.absarc(0, 0, radius1, 0, Math.PI * 2, false);

						// Use `THREE.ExtrudeGeometry` to sweep the profile along the contour
						const extrudeSettings1 = {
							steps: 100,
							extrudePath: contourPath // Sweep the profile along this path
						};

						geometry = new ExtrudeGeometry(profileShape, extrudeSettings1);

					break;

					case "profiledContour":
						let contour3;
						let semiCircleProfile;
						let material = this._settings.defaultMaterial.content.main[ quality ]
						material.side = DoubleSide;
						let materials = [this._settings.defaultMaterial.content.main[ quality ]]
						// Beveled edges
						const profileShape4 = [ -0.025, 0.025, 0.025, 0.025, 0.025, 0,  0, -0.025, -0.025,-0.025 ];
						const settings = {
							contour: contour3,
							contourShape: "round",  //rectangle, round, oval, hexagon
							contourDimensions: { x:1, y:1, z:64 },
							contourMaterials: materials,
							contourCap: true,
							contourClosed: true,
				
							profile: profileShape4, // semiCircleProfile,  //beveledEdgesProfile, //
							profileMaterialType: "group", //group //segment //band //face
							profileMaterials: materials,
							profileCap: false,
							profileSmoothShading: false,
							profileAlignment: "min", //alignment in x direction: min, center, max
							profileMappingType: "box" // box, cylinder, plane - Not implemented yet
						}
						const profiledContour = buildProfiledContour(settings)
						console.log( profiledContour )
						mesh = profiledContour;
					break

					case "default": 
						console.warn("unknown shape for GeneratedMesh:", shape);
					break;
				}

				if( !mesh && geomrty ){

					// Create the mesh
					mesh = new Mesh( geometry, this._settings.defaultMaterial.content.main[ quality ] );
				}else{
					console.error( "could not find generated mesh or geometry")
				}

				this._meshObject = mesh;

				// Get the mesh from the GTLF file based on the meshName in je pkg json settings
				// this._meshObject = dependencies.gltf.content.main[ quality ].gltf[ this._settings.meshName ];
				// this._meshObject.mesh.material = dependencies.defaultMaterial.content.main[ quality ];
				
				// //tiling multiplier
				// if ( this.tilingMultiplier ){

				//    for ( let mapType of [ 'bumpMap', 'emissiveMap', 'map', 'specularMap', 'roughnessMap', 'metalnessMap', 'normalMap', 'aoMap' ])
				// 	{
				// 		if ( this._meshObject.mesh.material[ mapType ] )
				// 		{
							
				// 			this._meshObject.mesh.material = this._meshObject.mesh.material.clone();
				// 			this._meshObject.mesh.material[ mapType ] = this._meshObject.mesh.material[ mapType ].clone();
				// 			this._meshObject.mesh.material[ mapType ].repeat.x *= this._settings.tilingMultiplier;
				// 			this._meshObject.mesh.material[ mapType ].repeat.y *= this._settings.tilingMultiplier;
				// 			this._meshObject.mesh.material[ mapType ].needsUpdate = true;
				// 		}
				// 	}
				// }

				// if( this._aoMap ){
				// 	//console.log( this._aoMap)
				//    this._meshObject.mesh.material.aoMap = this._aoMap.content.main[ quality ];
				//    this._meshObject.mesh.material.aoMap.needsUpdate = true;
				//    //this._meshObject.mesh.material.aoMap.channel = 0;
				// }

				// if( this._aoMapIntensity ){
				// 	this._meshObject.mesh.material.aoMapIntensity = this._aoMapIntensity
				// 	this._meshObject.mesh.material.aoMap.needsUpdate = true;
				// }

				// if( this._normalMap ){

				// 	this.buildMeshMaterial( this._meshObject.mesh.material )

				// }


				// this._meshObject.mesh.castShadow = true;
				// this._meshObject.mesh.receiveShadow = true;


				// // const mesh = new Mesh(
				// //     dependencies.geometry.content.main[ quality ],
				// //     dependencies.defaultMaterial.content.main[ quality ]
				// // );

				this._setContent( 'main', quality, this._meshObject );

			break;

			default:

				this._setContent(part, quality, null);
				
			break;

		}

	}


}

export { GeneratedMesh };