import { Quaternion } from '../math/Quaternion.js';

/**
 *
 * Buffered scene graph property that allows weighted accumulation.
 *
 *
 * @author Ben Houston / http://clara.io/
 * @author David Sarno / http://lighthaus.us/
 * @author tschw
 */

function PropertyMixer( binding, typeName, valueSize ) {

	this.binding = binding;
	this.valueSize = valueSize;

	var mixFunction,
		mixFunctionAdditive,
		setIdentity;

	// buffer layout: [ incoming | accu0 | accu1 | orig | addAccu | (optional work) ]
	//
	// interpolators can use .buffer as their .result
	// the data then goes to 'incoming'
	//
	// 'accu0' and 'accu1' are used frame-interleaved for
	// the cumulative result and are compared to detect
	// changes
	//
	// 'orig' stores the original state of the property
	//
	// 'add' is used for additive cumulative results
	//
	// 'work' is optional and is only present for quaternion types. It is used
	// to store intermediate quaternion multiplication results

	switch ( typeName ) {

		case 'quaternion':
			mixFunction = this._slerp;
			mixFunctionAdditive = this._slerpAdditive;
			setIdentity = this._setAdditiveIdentityQuaternion;

			this.buffer = new Float64Array( valueSize * 6 );
			this._workIndex = 5;
			break;

		case 'string':
		case 'bool':
			mixFunction = this._select;

			// Use the regular mix function and for additive on these types,
			// additive is not relevant for non-numeric types
			mixFunctionAdditive = this._select;

			setIdentity = this._setAdditiveIdentityOther;

			this.buffer = new Array( valueSize * 5 );
			break;

		default:
			mixFunction = this._lerp;
			mixFunctionAdditive = this._lerpAdditive;
			setIdentity = this._setAdditiveIdentityNumeric;

			this.buffer = new Float64Array( valueSize * 5 );

	}

	this._mixBufferRegion = mixFunction;
	this._mixBufferRegionAdditive = mixFunctionAdditive;
	this._setIdentity = setIdentity;
	this._origIndex = 3;
	this._addIndex = 4;

	this.cumulativeWeight = 0;
	this.cumulativeWeightAdditive = 0;

	this.useCount = 0;
	this.referenceCount = 0;

}

Object.assign( PropertyMixer.prototype, {

	// accumulate data in the 'incoming' region into 'accu<i>'
	accumulate: function ( accuIndex, weight ) {

		// note: happily accumulating nothing when weight = 0, the caller knows
		// the weight and shouldn't have made the call in the first place

		var buffer = this.buffer,
			stride = this.valueSize,
			offset = accuIndex * stride + stride,

			currentWeight = this.cumulativeWeight;

		if ( currentWeight === 0 ) {

			// accuN := incoming * weight

			for ( var i = 0; i !== stride; ++ i ) {

				buffer[ offset + i ] = buffer[ i ];

			}

			currentWeight = weight;

		} else {

			// accuN := accuN + incoming * weight

			currentWeight += weight;
			var mix = weight / currentWeight;
			this._mixBufferRegion( buffer, offset, 0, mix, stride );

		}

		this.cumulativeWeight = currentWeight;

	},

	// accumulate data in the 'incoming' region into 'add'
	accumulateAdditive: function ( weight ) {

		var buffer = this.buffer,
			stride = this.valueSize,
			offset = stride * this._addIndex;

		if ( this.cumulativeWeightAdditive === 0 ) {

			// add = identity

			this._setIdentity();

		}

		// add := add + incoming * weight

		this._mixBufferRegionAdditive( buffer, offset, 0, weight, stride );
		this.cumulativeWeightAdditive += weight;

	},

	// apply the state of 'accu<i>' to the binding when accus differ
	apply: function ( accuIndex ) {

		var stride = this.valueSize,
			buffer = this.buffer,
			offset = accuIndex * stride + stride,

			weight = this.cumulativeWeight,
			weightAdditive = this.cumulativeWeightAdditive,

			binding = this.binding;

		this.cumulativeWeight = 0;
		this.cumulativeWeightAdditive = 0;

		if ( weight < 1 ) {

			// accuN := accuN + original * ( 1 - cumulativeWeight )

			var originalValueOffset = stride * this._origIndex;

			this._mixBufferRegion(
				buffer, offset, originalValueOffset, 1 - weight, stride );

		}

		if ( weightAdditive > 0 ) {

			// accuN := accuN + additive accuN

			this._mixBufferRegionAdditive( buffer, offset, this._addIndex * stride, 1, stride );

		}

		for ( var i = stride, e = stride + stride; i !== e; ++ i ) {

			if ( buffer[ i ] !== buffer[ i + stride ] ) {

				// value has changed -> update scene graph

				binding.setValue( buffer, offset );
				break;

			}

		}

	},

	// remember the state of the bound property and copy it to both accus
	saveOriginalState: function () {

		var binding = this.binding;

		var buffer = this.buffer,
			stride = this.valueSize,

			originalValueOffset = stride * this._origIndex;

		binding.getValue( buffer, originalValueOffset );

		// accu[0..1] := orig -- initially detect changes against the original
		for ( var i = stride, e = originalValueOffset; i !== e; ++ i ) {

			buffer[ i ] = buffer[ originalValueOffset + ( i % stride ) ];

		}

		// Add to identity for additive
		this._setIdentity();

		this.cumulativeWeight = 0;
		this.cumulativeWeightAdditive = 0;

	},

	// apply the state previously taken via 'saveOriginalState' to the binding
	restoreOriginalState: function () {

		var originalValueOffset = this.valueSize * 3;
		this.binding.setValue( this.buffer, originalValueOffset );

	},

	_setAdditiveIdentityNumeric: function () {

		var startIndex = this._addIndex * this.valueSize;

		this.buffer.fill( 0, startIndex, startIndex + this.valueSize );

	},

	_setAdditiveIdentityQuaternion: function () {

		this._setAdditiveIdentityNumeric();
		this.buffer[ this._addIndex * 4 + 3 ] = 1;

	},

	_setAdditiveIdentityOther: function () {

		var startIndex = this._origIndex * this.valueSize;
		var targetIndex = this._addIndex * this.valueSize;

		this.buffer.copyWithin( targetIndex, startIndex, this.valueSize );

	},


	// mix functions

	_select: function ( buffer, dstOffset, srcOffset, t, stride ) {

		if ( t >= 0.5 ) {

			for ( var i = 0; i !== stride; ++ i ) {

				buffer[ dstOffset + i ] = buffer[ srcOffset + i ];

			}

		}

	},

	_slerp: function ( buffer, dstOffset, srcOffset, t ) {

		Quaternion.slerpFlat( buffer, dstOffset, buffer, dstOffset, buffer, srcOffset, t );

	},

	_slerpAdditive: function ( buffer, dstOffset, srcOffset, t, stride ) {

		var workOffset = this._workIndex * stride;

		// Store result in intermediate buffer offset
		Quaternion.multiplyQuaternionsFlat( buffer, workOffset, buffer, dstOffset, buffer, srcOffset );

		// Slerp to the intermediate result
		Quaternion.slerpFlat( buffer, dstOffset, buffer, dstOffset, buffer, workOffset, t );

	},

	_lerp: function ( buffer, dstOffset, srcOffset, t, stride ) {

		var s = 1 - t;

		for ( var i = 0; i !== stride; ++ i ) {

			var j = dstOffset + i;

			buffer[ j ] = buffer[ j ] * s + buffer[ srcOffset + i ] * t;

		}

	},

	_lerpAdditive: function ( buffer, dstOffset, srcOffset, t, stride ) {

		for ( var i = 0; i !== stride; ++ i ) {

			var j = dstOffset + i;

			buffer[ j ] = buffer[ j ] + buffer[ srcOffset + i ] * t;

		}

	}

} );


export { PropertyMixer };
