/**
 * @author Kai Salmen / https://kaisalmen.de
 * Development repository: https://github.com/kaisalmen/WWOBJLoader
 */

import {
	BufferAttribute,
	BufferGeometry,
	LineSegments,
	Mesh,
	Points
} from "../../../../../build/three.module.js";


/**
 *
 * @param {MaterialHandler} materialHandler
 * @constructor
 */
const MeshReceiver = function ( materialHandler ) {

	this.logging = {
		enabled: false,
		debug: false
	};

	this.callbacks = {
		onProgress: null,
		onMeshAlter: null
	};
	this.materialHandler = materialHandler;

};

MeshReceiver.prototype = {

	constructor: MeshReceiver,

	/**
	 * Enable or disable logging in general (except warn and error), plus enable or disable debug logging.
	 *
	 * @param {boolean} enabled True or false.
	 * @param {boolean} debug True or false.
	 */
	setLogging:	function ( enabled, debug ) {

		this.logging.enabled = enabled === true;
		this.logging.debug = debug === true;

	},

	/**
	 *
	 * @param {Function} onProgress
	 * @param {Function} onMeshAlter
	 * @private
	 */
	_setCallbacks: function ( onProgress, onMeshAlter ) {

		if ( onProgress !== null && onProgress !== undefined && onProgress instanceof Function ) {

			this.callbacks.onProgress = onProgress;

		}

		if ( onMeshAlter !== null && onMeshAlter !== undefined && onMeshAlter instanceof Function ) {

			this.callbacks.onMeshAlter = onMeshAlter;

		}

	},

	/**
	 * Builds one or multiple meshes from the data described in the payload (buffers, params, material info).
	 *
	 * @param {Object} meshPayload Raw mesh description (buffers, params, materials) used to build one to many meshes.
	 * @returns {Mesh[]} mesh Array of {@link Mesh}
	 */
	buildMeshes: function ( meshPayload ) {

		let meshName = meshPayload.params.meshName;
		let buffers = meshPayload.buffers;

		let bufferGeometry = new BufferGeometry();
		if ( buffers.vertices !== undefined && buffers.vertices !== null ) {

			bufferGeometry.setAttribute( 'position', new BufferAttribute( new Float32Array( buffers.vertices ), 3 ) );

		}

		if ( buffers.indices !== undefined && buffers.indices !== null ) {

			bufferGeometry.setIndex( new BufferAttribute( new Uint32Array( buffers.indices ), 1 ) );

		}

		if ( buffers.colors !== undefined && buffers.colors !== null ) {

			bufferGeometry.setAttribute( 'color', new BufferAttribute( new Float32Array( buffers.colors ), 3 ) );

		}

		if ( buffers.normals !== undefined && buffers.normals !== null ) {

			bufferGeometry.setAttribute( 'normal', new BufferAttribute( new Float32Array( buffers.normals ), 3 ) );

		} else {

			bufferGeometry.computeVertexNormals();

		}

		if ( buffers.uvs !== undefined && buffers.uvs !== null ) {

			bufferGeometry.setAttribute( 'uv', new BufferAttribute( new Float32Array( buffers.uvs ), 2 ) );

		}

		if ( buffers.skinIndex !== undefined && buffers.skinIndex !== null ) {

			bufferGeometry.setAttribute( 'skinIndex', new BufferAttribute( new Uint16Array( buffers.skinIndex ), 4 ) );

		}

		if ( buffers.skinWeight !== undefined && buffers.skinWeight !== null ) {

			bufferGeometry.setAttribute( 'skinWeight', new BufferAttribute( new Float32Array( buffers.skinWeight ), 4 ) );

		}

		let material, materialName, key;
		let materialNames = meshPayload.materials.materialNames;
		let createMultiMaterial = meshPayload.materials.multiMaterial;
		let multiMaterials = [];

		for ( key in materialNames ) {

			materialName = materialNames[ key ];
			material = this.materialHandler.getMaterial( materialName );
			if ( createMultiMaterial ) multiMaterials.push( material );

		}

		if ( createMultiMaterial ) {

			material = multiMaterials;
			let materialGroups = meshPayload.materials.materialGroups;
			let materialGroup;
			for ( key in materialGroups ) {

				materialGroup = materialGroups[ key ];
				bufferGeometry.addGroup( materialGroup.start, materialGroup.count, materialGroup.index );

			}

		}

		let meshes = [];
		let mesh;
		let callbackOnMeshAlterResult;
		let useOrgMesh = true;
		let geometryType = meshPayload.geometryType === null ? 0 : meshPayload.geometryType;

		if ( this.callbacks.onMeshAlter ) {

			callbackOnMeshAlterResult = this.callbacks.onMeshAlter(
				{
					detail: {
						meshName: meshName,
						bufferGeometry: bufferGeometry,
						material: material,
						geometryType: geometryType
					}
				}
			);

		}

		// here LoadedMeshUserOverride is required to be provided by the callback used to alter the results
		if ( callbackOnMeshAlterResult ) {

			if ( callbackOnMeshAlterResult.isDisregardMesh() ) {

				useOrgMesh = false;

			} else if ( callbackOnMeshAlterResult.providesAlteredMeshes() ) {

				for ( let i in callbackOnMeshAlterResult.meshes ) {

					meshes.push( callbackOnMeshAlterResult.meshes[ i ] );

				}

				useOrgMesh = false;

			}

		}

		if ( useOrgMesh ) {

			if ( meshPayload.computeBoundingSphere ) bufferGeometry.computeBoundingSphere();
			if ( geometryType === 0 ) {

				mesh = new Mesh( bufferGeometry, material );

			} else if ( geometryType === 1 ) {

				mesh = new LineSegments( bufferGeometry, material );

			} else {

				mesh = new Points( bufferGeometry, material );

			}

			mesh.name = meshName;
			meshes.push( mesh );

		}

		let progressMessage = meshPayload.params.meshName;
		if ( meshes.length > 0 ) {

			let meshNames = [];
			for ( let i in meshes ) {

				mesh = meshes[ i ];
				meshNames[ i ] = mesh.name;

			}

			progressMessage += ': Adding mesh(es) (' + meshNames.length + ': ' + meshNames + ') from input mesh: ' + meshName;
			progressMessage += ' (' + ( meshPayload.progress.numericalValue * 100 ).toFixed( 2 ) + '%)';

		} else {

			progressMessage += ': Not adding mesh: ' + meshName;
			progressMessage += ' (' + ( meshPayload.progress.numericalValue * 100 ).toFixed( 2 ) + '%)';

		}

		if ( this.callbacks.onProgress ) {

			this.callbacks.onProgress( 'progress', progressMessage, meshPayload.progress.numericalValue );

		}

		return meshes;

	}

};

/**
 * Object to return by callback onMeshAlter. Used to disregard a certain mesh or to return one to many meshes.
 * @class
 *
 * @param {boolean} disregardMesh=false Tell implementation to completely disregard this mesh
 * @param {boolean} disregardMesh=false Tell implementation that mesh(es) have been altered or added
 */
const LoadedMeshUserOverride = function ( disregardMesh, alteredMesh ) {

	this.disregardMesh = disregardMesh === true;
	this.alteredMesh = alteredMesh === true;
	this.meshes = [];

};


LoadedMeshUserOverride.prototype = {

	constructor: LoadedMeshUserOverride,

	/**
	 * Add a mesh created within callback.
	 *
	 * @param {Mesh} mesh
	 */
	addMesh: function ( mesh ) {

		this.meshes.push( mesh );
		this.alteredMesh = true;

	},

	/**
	 * Answers if mesh shall be disregarded completely.
	 *
	 * @returns {boolean}
	 */
	isDisregardMesh: function () {

		return this.disregardMesh;

	},

	/**
	 * Answers if new mesh(es) were created.
	 *
	 * @returns {boolean}
	 */
	providesAlteredMeshes: function () {

		return this.alteredMesh;

	}
};

export {
	MeshReceiver,
	LoadedMeshUserOverride
};
