import NodeMaterial from './NodeMaterial.js';
import { materialRotation } from '../../nodes/accessors/MaterialNode.js';
import { modelViewMatrix, modelWorldMatrix } from '../../nodes/accessors/ModelNode.js';
import { positionGeometry } from '../../nodes/accessors/Position.js';
import { rotate } from '../../nodes/utils/RotateNode.js';
import { float, vec2, vec3, vec4 } from '../../nodes/tsl/TSLBase.js';

import { SpriteMaterial } from '../SpriteMaterial.js';
import { reference } from '../../nodes/accessors/ReferenceBaseNode.js';

const _defaultValues = /*@__PURE__*/ new SpriteMaterial();

/**
 * Node material version of {@link SpriteMaterial}.
 *
 * @augments NodeMaterial
 */
class SpriteNodeMaterial extends NodeMaterial {

	static get type() {

		return 'SpriteNodeMaterial';

	}

	/**
	 * Constructs a new sprite node material.
	 *
	 * @param {Object} [parameters] - The configuration parameter.
	 */
	constructor( parameters ) {

		super();

		/**
		 * This flag can be used for type testing.
		 *
		 * @type {boolean}
		 * @readonly
		 * @default true
		 */
		this.isSpriteNodeMaterial = true;

		this._useSizeAttenuation = true;

		/**
		 * This property makes it possible to define the position of the sprite with a
		 * node. That can be useful when the material is used with instanced rendering
		 * and node data are defined with an instanced attribute node:
		 * ```js
		 * const positionAttribute = new InstancedBufferAttribute( new Float32Array( positions ), 3 );
		 * material.positionNode = instancedBufferAttribute( positionAttribute );
		 * ```
		 * Another possibility is to compute the instanced data with a compute shader:
		 * ```js
		 * const positionBuffer = instancedArray( particleCount, 'vec3' );
		 * particleMaterial.positionNode = positionBuffer.toAttribute();
		 * ```
		 *
		 * @type {?Node<vec2>}
		 * @default null
		 */
		this.positionNode = null;

		/**
		 * The rotation of sprite materials is by default inferred from the `rotation`,
		 * property. This node property allows to overwrite the default and define
		 * the rotation with a node instead.
		 *
		 * If you don't want to overwrite the rotation but modify the existing
		 * value instead, use {@link materialRotation}.
		 *
		 * @type {?Node<float>}
		 * @default null
		 */
		this.rotationNode = null;

		/**
		 * This node property provides an additional way to scale sprites next to
		 * `Object3D.scale`. The scale transformation based in `Object3D.scale`
		 * is multiplied with the scale value of this node in the vertex shader.
		 *
		 * @type {?Node<vec2>}
		 * @default null
		 */
		this.scaleNode = null;

		/**
		 * In Sprites, the transparent property is enabled by default.
		 *
		 * @type {boolean}
		 * @default true
		 */
		this.transparent = true;

		this.setDefaultValues( _defaultValues );

		this.setValues( parameters );

	}

	/**
	 * Setups the position node in view space. This method implements
	 * the sprite specific vertex shader.
	 *
	 * @param {NodeBuilder} builder - The current node builder.
	 * @return {Node<vec3>} The position in view space.
	 */
	setupPositionView( builder ) {

		const { object, camera } = builder;

		const { positionNode, rotationNode, scaleNode, sizeAttenuation } = this;

		const mvPosition = modelViewMatrix.mul( vec3( positionNode || 0 ) );

		let scale = vec2( modelWorldMatrix[ 0 ].xyz.length(), modelWorldMatrix[ 1 ].xyz.length() );

		if ( scaleNode !== null ) {

			scale = scale.mul( vec2( scaleNode ) );

		}

		if ( camera.isPerspectiveCamera && sizeAttenuation === false ) {

			scale = scale.mul( mvPosition.z.negate() );

		}

		let alignedPosition = positionGeometry.xy;

		if ( object.center && object.center.isVector2 === true ) {

			const center = reference( 'center', 'vec2', object );

			alignedPosition = alignedPosition.sub( center.sub( 0.5 ) );

		}

		alignedPosition = alignedPosition.mul( scale );

		const rotation = float( rotationNode || materialRotation );

		const rotatedPosition = rotate( alignedPosition, rotation );

		return vec4( mvPosition.xy.add( rotatedPosition ), mvPosition.zw );

	}

	copy( source ) {

		this.positionNode = source.positionNode;
		this.rotationNode = source.rotationNode;
		this.scaleNode = source.scaleNode;

		return super.copy( source );

	}

	/**
	 * Whether to use size attenuation or not.
	 *
	 * @type {boolean}
	 * @default true
	 */
	get sizeAttenuation() {

		return this._useSizeAttenuation;

	}

	set sizeAttenuation( value ) {

		if ( this._useSizeAttenuation !== value ) {

			this._useSizeAttenuation = value;
			this.needsUpdate = true;

		}

	}

}

export default SpriteNodeMaterial;
