/**
 * Development repository: https://github.com/kaisalmen/WWOBJLoader
 */

import {
	FileLoader,
	Object3D,
	Loader
} from '../../../build/three.module.js';

import { OBJLoader2Parser } from './obj2/OBJLoader2Parser.js';
import { MeshReceiver } from './obj2/shared/MeshReceiver.js';
import { MaterialHandler } from './obj2/shared/MaterialHandler.js';

/**
 * Creates a new OBJLoader2. Use it to load OBJ data from files or to parse OBJ data from arraybuffer or text.
 *
 * @param {LoadingManager} [manager] The loadingManager for the loader to use. Default is {@link LoadingManager}
 * @constructor
 */
const OBJLoader2 = function ( manager ) {

	Loader.call( this, manager );

	this.parser = new OBJLoader2Parser();

	this.modelName = '';
	this.instanceNo = 0;
	this.baseObject3d = new Object3D();

	this.materialHandler = new MaterialHandler();
	this.meshReceiver = new MeshReceiver( this.materialHandler );

	// as OBJLoader2 is no longer derived from OBJLoader2Parser, we need to override the default onAssetAvailable callback
	const scope = this;
	const defaultOnAssetAvailable = function ( payload ) {

		scope._onAssetAvailable( payload );

	};

	this.parser.setCallbackOnAssetAvailable( defaultOnAssetAvailable );

};

OBJLoader2.OBJLOADER2_VERSION = '3.2.0';
console.info( 'Using OBJLoader2 version: ' + OBJLoader2.OBJLOADER2_VERSION );


OBJLoader2.prototype = Object.assign( Object.create( Loader.prototype ), {

	constructor: OBJLoader2,

	/**
	 * See {@link OBJLoader2Parser.setLogging}
	 * @return {OBJLoader2}
	 */
	setLogging: function ( enabled, debug ) {

		this.parser.setLogging( enabled, debug );
		return this;

	},

	/**
	 * See {@link OBJLoader2Parser.setMaterialPerSmoothingGroup}
	 * @return {OBJLoader2}
	 */
	setMaterialPerSmoothingGroup: function ( materialPerSmoothingGroup ) {

		this.parser.setMaterialPerSmoothingGroup( materialPerSmoothingGroup );
		return this;

	},

	/**
	 * See {@link OBJLoader2Parser.setUseOAsMesh}
	 * @return {OBJLoader2}
	 */
	setUseOAsMesh: function ( useOAsMesh ) {

		this.parser.setUseOAsMesh( useOAsMesh );
		return this;

	},

	/**
	 * See {@link OBJLoader2Parser.setUseIndices}
	 * @return {OBJLoader2}
	 */
	setUseIndices: function ( useIndices ) {

		this.parser.setUseIndices( useIndices );
		return this;

	},

	/**
	 * See {@link OBJLoader2Parser.setDisregardNormals}
	 * @return {OBJLoader2}
	 */
	setDisregardNormals: function ( disregardNormals ) {

		this.parser.setDisregardNormals( disregardNormals );
		return this;

	},

	/**
	 * Set the name of the model.
	 *
	 * @param {string} modelName
	 * @return {OBJLoader2}
	 */
	setModelName: function ( modelName ) {

		this.modelName = modelName ? modelName : this.modelName;
		return this;

	},

	/**
	 * Set the node where the loaded objects will be attached directly.
	 *
	 * @param {Object3D} baseObject3d Object already attached to scenegraph where new meshes will be attached to
	 * @return {OBJLoader2}
	 */
	setBaseObject3d: function ( baseObject3d ) {

		this.baseObject3d = ( baseObject3d === undefined || baseObject3d === null ) ? this.baseObject3d : baseObject3d;
		return this;

	},

	/**
	 * Add materials as associated array.
	 *
	 * @param {Object} materials Object with named {@link Material}
	 * @param overrideExisting boolean Override existing material
	 * @return {OBJLoader2}
	 */
	addMaterials: function ( materials, overrideExisting ) {

		this.materialHandler.addMaterials( materials, overrideExisting );
		return this;

	},

	/**
	 * See {@link OBJLoader2Parser.setCallbackOnAssetAvailable}
	 * @return {OBJLoader2}
	 */
	setCallbackOnAssetAvailable: function ( onAssetAvailable ) {

		this.parser.setCallbackOnAssetAvailable( onAssetAvailable );
		return this;

	},

	/**
	 * See {@link OBJLoader2Parser.setCallbackOnProgress}
	 * @return {OBJLoader2}
	 */
	setCallbackOnProgress: function ( onProgress ) {

		this.parser.setCallbackOnProgress( onProgress );
		return this;

	},

	/**
	 * See {@link OBJLoader2Parser.setCallbackOnError}
	 * @return {OBJLoader2}
	 */
	setCallbackOnError: function ( onError ) {

		this.parser.setCallbackOnError( onError );
		return this;

	},

	/**
	 * See {@link OBJLoader2Parser.setCallbackOnLoad}
	 * @return {OBJLoader2}
	 */
	setCallbackOnLoad: function ( onLoad ) {

		this.parser.setCallbackOnLoad( onLoad );
		return this;

	},

	/**
	 * Register a function that is called once a single mesh is available and it could be altered by the supplied function.
	 *
	 * @param {Function} [onMeshAlter]
	 * @return {OBJLoader2}
	 */
	setCallbackOnMeshAlter: function ( onMeshAlter ) {

		this.meshReceiver._setCallbacks( this.parser.callbacks.onProgress, onMeshAlter );
		return this;

	},

	/**
	 * Register a function that is called once all materials have been loaded and they could be altered by the supplied function.
	 *
	 * @param {Function} [onLoadMaterials]
	 * @return {OBJLoader2}
	 */
	setCallbackOnLoadMaterials: function ( onLoadMaterials ) {

		this.materialHandler._setCallbacks( onLoadMaterials );
		return this;

	},

	/**
	 * Use this convenient method to load a file at the given URL. By default the fileLoader uses an ArrayBuffer.
	 *
	 * @param {string}  url A string containing the path/URL of the file to be loaded.
	 * @param {function} onLoad A function to be called after loading is successfully completed. The function receives loaded Object3D as an argument.
	 * @param {function} [onFileLoadProgress] A function to be called while the loading is in progress. The argument will be the XMLHttpRequest instance, which contains total and Integer bytes.
	 * @param {function} [onError] A function to be called if an error occurs during loading. The function receives the error as an argument.
	 * @param {function} [onMeshAlter] Called after every single mesh is made available by the parser
	 */
	load: function ( url, onLoad, onFileLoadProgress, onError, onMeshAlter ) {

		const scope = this;
		if ( onLoad === null || onLoad === undefined || ! ( onLoad instanceof Function ) ) {

			const errorMessage = 'onLoad is not a function! Aborting...';
			scope.parser.callbacks.onError( errorMessage );
			throw errorMessage;

		} else {

			this.parser.setCallbackOnLoad( onLoad );

		}

		if ( onError === null || onError === undefined || ! ( onError instanceof Function ) ) {

			onError = function ( event ) {

				let errorMessage = event;

				if ( event.currentTarget && event.currentTarget.statusText !== null ) {

					errorMessage = 'Error occurred while downloading!\nurl: ' + event.currentTarget.responseURL + '\nstatus: ' + event.currentTarget.statusText;

				}

				scope.parser.callbacks.onError( errorMessage );

			};

		}

		if ( ! url ) {

			onError( 'An invalid url was provided. Unable to continue!' );

		}

		const urlFull = new URL( url, window.location.href ).href;
		let filename = urlFull;
		const urlParts = urlFull.split( '/' );
		if ( urlParts.length > 2 ) {

			filename = urlParts[ urlParts.length - 1 ];
			this.path = urlParts.slice( 0, urlParts.length - 1 ).join( '/' ) + '/';

		}

		if ( onFileLoadProgress === null || onFileLoadProgress === undefined || ! ( onFileLoadProgress instanceof Function ) ) {

			let numericalValueRef = 0;
			let numericalValue = 0;
			onFileLoadProgress = function ( event ) {

				if ( ! event.lengthComputable ) return;

				numericalValue = event.loaded / event.total;

				if ( numericalValue > numericalValueRef ) {

					numericalValueRef = numericalValue;
					const output = 'Download of "' + url + '": ' + ( numericalValue * 100 ).toFixed( 2 ) + '%';
					scope.parser.callbacks.onProgress( 'progressLoad', output, numericalValue );

				}

			};

		}

		this.setCallbackOnMeshAlter( onMeshAlter );
		const fileLoaderOnLoad = function ( content ) {

			scope.parser.callbacks.onLoad( scope.parse( content ), 'OBJLoader2#load: Parsing completed' );

		};

		const fileLoader = new FileLoader( this.manager );
		fileLoader.setPath( this.path || this.resourcePath );
		fileLoader.setResponseType( 'arraybuffer' );
		fileLoader.load( filename, fileLoaderOnLoad, onFileLoadProgress, onError );

	},

	/**
	 * Parses OBJ data synchronously from arraybuffer or string and returns the {@link Object3D}.
	 *
	 * @param {arraybuffer|string} content OBJ data as Uint8Array or String
	 * @return {Object3D}
	 */
	parse: function ( content ) {

		// fast-fail in case of illegal data
		if ( content === null || content === undefined ) {

			throw 'Provided content is not a valid ArrayBuffer or String. Unable to continue parsing';

		}

		if ( this.parser.logging.enabled ) {

			console.time( 'OBJLoader parse: ' + this.modelName );

		}

		// Create default materials beforehand, but do not override previously set materials (e.g. during init)
		this.materialHandler.createDefaultMaterials( false );

		// code works directly on the material references, parser clear its materials before updating
		this.parser.setMaterials( this.materialHandler.getMaterials() );

		if ( content instanceof ArrayBuffer || content instanceof Uint8Array ) {

			if ( this.parser.logging.enabled ) console.info( 'Parsing arrayBuffer...' );
			this.parser.execute( content );

		} else if ( typeof ( content ) === 'string' || content instanceof String ) {

			if ( this.parser.logging.enabled ) console.info( 'Parsing text...' );
			this.parser.executeLegacy( content );

		} else {

			this.parser.callbacks.onError( 'Provided content was neither of type String nor Uint8Array! Aborting...' );

		}

		if ( this.parser.logging.enabled ) {

			console.timeEnd( 'OBJLoader parse: ' + this.modelName );

		}

		return this.baseObject3d;

	},

	_onAssetAvailable: function ( payload ) {

		if ( payload.cmd !== 'assetAvailable' ) return;

		if ( payload.type === 'mesh' ) {

			const meshes = this.meshReceiver.buildMeshes( payload );
			for ( const mesh of meshes ) {

				this.baseObject3d.add( mesh );

			}

		} else if ( payload.type === 'material' ) {

			this.materialHandler.addPayloadMaterials( payload );

		}

	}

} );

export { OBJLoader2 };
