import { Box, Matrix, Rectangle, Sphere, Vector3D } from '@awayjs/core';

import { AttributesBuffer, AttributesView, Byte4Attributes, Float1Attributes } from '@awayjs/stage';

import { View, PickingCollision, ContainerNode } from '@awayjs/view';

import { ElementsBase, THullImplId } from './ElementsBase';

const TMP_BBOX = new Box();

/**
 * @class LineElements
 */
export class LineElements extends ElementsBase {
	public static assetType: string = '[asset LineElements]';

	private _positions: AttributesView;
	private _thickness: Float1Attributes;
	private _colors: Byte4Attributes;
	private _thicknessScale: Vector3D = new Vector3D();

	//used for hittesting geometry
	public hitTestCache: Object = new Object();

	public half_thickness: number = 5;
	public scaleMode: LineScaleMode = LineScaleMode.HAIRLINE;
	public dimension: number = 3;

	public getThicknessScale(node: ContainerNode, strokeFlag: boolean): Vector3D {
		if (!strokeFlag && this.scaleMode == LineScaleMode.HAIRLINE) {
			this._thicknessScale.identity();
		} else {
			if (node)
				this._thicknessScale.copyFrom(node.getMatrix3D().decompose()[2]);
			else
				this._thicknessScale.identity();

			this._thicknessScale.x *= node.view.focalLength * node.view.pixelRatio / 1000;
			this._thicknessScale.y *= node.view.focalLength / 1000;

			if (this.scaleMode == LineScaleMode.NORMAL) {
				this._thicknessScale.x = (!strokeFlag || this.half_thickness * this._thicknessScale.x > 0.5)
					? this.half_thickness
					: this._thicknessScale.x ? 0.5 / this._thicknessScale.x : 0;
				this._thicknessScale.y = (!strokeFlag || this.half_thickness * this._thicknessScale.y > 0.5)
					? this.half_thickness
					: this._thicknessScale.y ? 0.5 / this._thicknessScale.y : 0;
			} else if (this.scaleMode == LineScaleMode.HAIRLINE) {
				this._thicknessScale.x = this._thicknessScale.x ? 0.5 / this._thicknessScale.x : 0;
				this._thicknessScale.y = this._thicknessScale.y ? 0.5 / this._thicknessScale.y : 0;
			}
		}

		return this._thicknessScale;
	}

	/**
	 *
	 * @returns {string}
	 */
	public get assetType(): string {
		return LineElements.assetType;
	}

	/**
	 *
	 */
	public get positions(): AttributesView {
		return this._positions;
	}

	/**
	 *
	 */
	public get thickness(): Float1Attributes {
		return this._thickness;
	}

	/**
	 *
	 */
	public get colors(): Byte4Attributes {
		if (!this._colors)
			this.setColors(this._colors);

		return this._colors;
	}

	/**
	 *
	 */
	constructor(concatenatedBuffer: AttributesBuffer = null) {
		super(concatenatedBuffer);

		this.dimension = Settings.LINE_BUFFER_DIM;
		this._positions = new AttributesView(Float32Array, this.dimension * 2, concatenatedBuffer);
	}

	public prepareScale9(
		bounds: Rectangle,
		grid: Rectangle,
		clone: boolean,
		_emitUV?: boolean,
		_uvMatrix?: Matrix
	): LineElements {
		return LineElementsUtils.prepareScale9(this, bounds, grid, clone);
	}

	public updateScale9(scaleX: number, scaleY: number) {
		if (!this.scale9Indices) {
			return;
		}

		LineElementsUtils.updateScale9(
			this,
			this.originalScale9Bounds,
			scaleX,
			scaleY,
			false,
			false
		);
	}

	public getBoxBounds(
		node: ContainerNode = null,
		strokeFlag: boolean = false,
		matrix3D: Matrix3D = null,
		cache: Box = null,
		target: Box = null,
		count: number = 0,
		offset: number = 0): Box

	// eslint-disable-next-line brace-style
	{

		count = count || this._numElements || this._numVertices;
		this._boundsRequests++;

		if (
			Settings.ENABLE_CONVEX_BOUNDS
			&& this._boundsRequests > Settings.CONVEX_MIN_REQUIEST_FOR_BUILD
			&& count > Settings.POINTS_COUNT_FOR_CONVEX
			&& !this.isDynamic // diable for dynamic elements, beacause a reconstructed every frame
		) {
			if (
				!this._convexHull
				|| this._convexHull.count !== count// drop hull data, invalid
				|| this._convexHull.offset !==  offset // drop hull data, invalid
			) {
				this._convexHull = <THullImplId> ConvexHullUtils.fromAttribute(
					this.positions,
					this.indices,
					3, // line elements has to many attirbutes, tick every 3
					count,
					offset
				);

				if (this._convexHull) {
					this._convexHull.offset = offset;
					this._convexHull.count = count;

					console.debug(
						'[Line] Build convex for:', this.id,
						'vertex / hull', (count / this._convexHull.points.length) | 0);
				}
			}

			if (this._convexHull) {
				const tmp = TMP_BBOX;
				tmp.identity();

				const box = ConvexHullUtils.createBox(
					this._convexHull,
					matrix3D,
					null,
					tmp
				);

				LineElementsUtils.mergeThinkness(
					box,
					this.getThicknessScale(node, strokeFlag),
					matrix3D
				);

				return tmp.union(target, target || cache);
			}
		}

		return LineElementsUtils.getBoxBounds(
			this.positions,
			this.indices,
			matrix3D,
			this.getThicknessScale(node, strokeFlag),
			cache,
			target,
			count,
			offset);
	}

	public getSphereBounds(
		center: Vector3D,
		matrix3D: Matrix3D = null,
		strokeFlag: boolean = false,
		cache: Sphere = null,
		target: Sphere = null,
		count: number = 0,
		offset: number = 0): Sphere

	// eslint-disable-next-line brace-style
	{
		return LineElementsUtils.getSphereBounds(
			this.positions, center, matrix3D, cache, target, count || this._numVertices, offset);
	}

	public hitTestPoint(
		node: ContainerNode,
		x: number, y: number, z: number,
		box: Box,
		count: number = 0,
		offset: number = 0,
		idx_count: number = 0,
		idx_offset: number = 0): boolean
	// eslint-disable-next-line brace-style
	{
		const scale: Vector3D = this.getThicknessScale(node, true);
		const thickness: number = (scale.x + scale.y) / 2;//approx hack for now

		return LineElementsUtils.hitTest(
			x, y, 0, thickness, box, this, count || this._numElements || this._numVertices, offset);
	}

	/**
	 *
	 */
	public setPositions(array: Array<number>, offset?: number): void;
	public setPositions(arrayBufferView: ArrayBufferView, offset?: number): void;
	public setPositions(attributesView: AttributesView, offset?: number): void;
	public setPositions(values: any, offset: number = 0): void {
		const dimension = this.dimension * 2;

		if (values instanceof AttributesView) {
			this.clearVertices(this._positions);
			this._positions = <AttributesView> values;
		} else if (values) {
			let i: number = 0;
			let j: number = 0;
			let index: number = 0;

			const positions: Float32Array = new Float32Array(dimension * 4 * (values.length / 6));
			const indices: Uint16Array = new Uint16Array(values.length);

			//oders incoming startpos/endpos values to look like the following 6-dimensional attributes view:
			//startpos x, y, z endpos x, y, z
			//endpos x, y, z startpos x, y, z
			//startpos x, y, z endpos x, y, z
			//endpos x, y, z startpos x, y, z
			while (i < values.length) {
				if (index / dimension & 1) { //if number is odd, reverse the order of startpos/endpos
					positions[index++] = values[i + 3];
					positions[index++] = values[i + 4];
					dimension === 6 && (positions[index++] = values[i + 5]);

					positions[index++] = values[i];
					positions[index++] = values[i + 1];
					dimension === 6 && (positions[index++] = values[i + 2]);
				} else {
					positions[index++] = values[i];
					positions[index++] = values[i + 1];
					dimension === 6 && (positions[index++] = values[i + 2]);

					positions[index++] = values[i + 3];
					positions[index++] = values[i + 4];
					dimension === 6 && (positions[index++] = values[i + 5]);
				}

				//index += 6;

				if (++j == 4) {
					const o: number = index / dimension - 4;
					indices.set([o, o + 1, o + 2, o + 3, o + 2, o + 1], i);
					j = 0;
					i += 6;
				}
			}

			this._positions.set(positions, offset * 4);

			this.setIndices(indices, offset);
		} else {
			this.clearVertices(this._positions);
			this._positions = new AttributesView(Float32Array, dimension, this._concatenatedBuffer);
		}

		this._numVertices = this._positions.count;

		this.invalidateVertices(this._positions);

		this._verticesDirty[this._positions.id] = false;
	}

	/**
	 * Updates the thickness.
	 */
	public setThickness(array: Array<number>, offset?: number);
	public setThickness(float32Array: Float32Array, offset?: number);
	public setThickness(float1Attributes: Float1Attributes, offset?: number);
	public setThickness(values: any, offset: number = 0): void {
		if (values instanceof Float1Attributes) {
			this._thickness = <Float1Attributes> values;
		} else if (values) {
			if (!this._thickness)
				this._thickness = new Float1Attributes(this._concatenatedBuffer);

			let i: number = 0;
			let j: number = 0;
			let index: number = 0;
			const thickness: Float32Array = new Float32Array(values.length * 4);

			//oders incoming thickness values to look like the following 1-dimensional attributes view:
			//thickness t
			//thickness -t
			//thickness -t
			//thickness t
			while (i < values.length) {
				thickness[index] = (Math.floor(0.5 * index + 0.5) & 1) ? -values[i] : values[i];

				if (++j == 4) {
					j = 0;
					i++;
				}

				index++;
			}

			this._thickness.set(thickness, offset * 4);
		} else if (this._thickness) {
			this._thickness.dispose();
			this._thickness = null;
		}

		this.invalidateVertices(this._thickness);

		this._verticesDirty[this._thickness.id] = false;
	}

	/**
	 *
	 */
	public setColors(array: Array<number>, offset?: number);
	public setColors(float32Array: Float32Array, offset?: number);
	public setColors(uint8Array: Uint8Array, offset?: number);
	public setColors(byte4Attributes: Byte4Attributes, offset?: number);
	public setColors(values: any, offset: number = 0): void {
		if (values) {
			if (values == this._colors)
				return;

			if (values instanceof Byte4Attributes) {
				this.clearVertices(this._colors);
				this._colors = <Byte4Attributes> values;
			} else {
				if (!this._colors)
					this._colors = new Byte4Attributes(this._concatenatedBuffer);

				let i: number = 0;
				let j: number = 0;
				let index: number = 0;
				const colors: Uint8Array = new Uint8Array(values.length * 4);

				while (i < values.length) {
					if (index / 4 & 1) {
						colors[index] = values[i + 4];
						colors[index + 1] = values[i + 5];
						colors[index + 2] = values[i + 6];
						colors[index + 3] = values[i + 7];
					} else {
						colors[index] = values[i];
						colors[index + 1] = values[i + 1];
						colors[index + 2] = values[i + 2];
						colors[index + 3] = values[i + 3];
					}

					if (++j == 4) {
						j = 0;
						i += 8;
					}

					index += 4;
				}

				this._colors.set(colors, offset * 4);
			}
		} else {
			//auto-derive colors
			this._colors = ElementsUtils.generateColors(
				this.indices, this._colors, this._concatenatedBuffer, this._numVertices);
		}

		this.invalidateVertices(this._colors);

		this._verticesDirty[this._colors.id] = false;
	}

	/**
	 *
	 */
	public dispose(): void {
		super.dispose();

		this._positions.dispose();
		this._positions = null;

		if (this._thickness) {
			this._thickness.dispose();
			this._thickness = null;
		}

		if (this._colors) {
			this._colors.dispose();
			this._colors = null;
		}
	}

	/**
	 * Clones the current object
	 * @return An exact duplicate of the current object.
	 */
	public clone(): LineElements {
		const clone: LineElements = new LineElements(
			this._concatenatedBuffer ? this._concatenatedBuffer.clone() : null);

		clone.setIndices(this.indices.clone());
		clone.setPositions(this._positions.clone());

		this._thickness && clone.setThickness(this._thickness.clone());
		this._colors && clone.setColors(this._colors.clone());

		return clone;
	}

	public testCollision(
		collision: PickingCollision,
		box: Box,
		closestFlag: boolean,
		material: IMaterial,
		count: number,
		offset: number = 0): boolean
	// eslint-disable-next-line brace-style
	{
		//TODO: peform correct line collision calculations
		const scale: Vector3D = this.getThicknessScale(collision.containerNode, true);
		const thickness: number = (scale.x + scale.y) / 2;//approx hack for now

		const rayEntryDistance: number = -collision.rayPosition.z / collision.rayDirection.z;
		const position: Vector3D = new Vector3D(
			collision.rayPosition.x + rayEntryDistance * collision.rayDirection.x,
			collision.rayPosition.y + rayEntryDistance * collision.rayDirection.y);

		//TODO use proper 3d testCollision method
		if (LineElementsUtils.hitTest(position.x, position.y, 0, thickness, box, this, this._numElements, offset)) {
			collision.rayEntryDistance = rayEntryDistance;
			collision.position = position;
			collision.normal = new Vector3D(0,0,1);

			return true;
		}

		return false;
	}
}

import { Matrix3D } from '@awayjs/core';

import {
	ContextGLDrawMode,
	IContextGL,
	ContextGLProgramType,
	Stage,
	ShaderRegisterCache,
	ShaderRegisterElement,
	ShaderRegisterData,
	IVao
} from '@awayjs/stage';

import { ElementsUtils } from '../utils/ElementsUtils';
import { IMaterial } from '../base/IMaterial';
import { _Stage_ElementsBase } from '../base/_Stage_ElementsBase';
import { _Render_RenderableBase } from '../base/_Render_RenderableBase';
import { ShaderBase } from '../base/ShaderBase';
import { _Render_ElementsBase } from '../base/_Render_ElementsBase';
import { RenderGroup } from '../RenderGroup';
import { LineElementsUtils } from '../utils/LineElementsUtils';
import { ConvexHullUtils } from '../utils/ConvexHullUtils';
import { Settings } from '../Settings';
import { LineScaleMode } from './LineScaleMode';

/**
 *
 * @class away.pool._Stage_LineElements
 */
export class _Stage_LineElements extends _Stage_ElementsBase {
	private _scale: Vector3D = new Vector3D();
	private _vao: IVao;
	private _vaoIsInvalid: boolean = true;

	public get lineElements(): LineElements {
		return this._useWeak ? (<WeakRef<LineElements>> this._asset).deref() : <LineElements> this._asset;
	}

	public init(lineElements: LineElements, stage: Stage): void {
		super.init(lineElements, stage);

		this._vao = (stage.context.hasVao && Settings.ALLOW_VAO && !lineElements.isDynamic)
			? stage.context.createVao()
			: null;
	}

	public onInvalidate() {
		super.onInvalidate();

		this._vaoIsInvalid = true;
	}

	public onClear(): void {
		super.onClear();

		this._vaoIsInvalid = true;
		if (this._vao) {
			this._vao.dispose();
			this._vao = null;
		}
	}

	public transformMatrix(renderRenderable: _Render_RenderableBase, matrix: Matrix): Matrix {
		const matrix3D: Matrix3D = new Matrix3D();
		matrix3D.copyFrom(renderRenderable.entity.node.getMatrix3D());
		matrix3D.append(renderRenderable.entity.renderer.view.projection.transform.inverseMatrix3D);
		const offset: Vector3D = matrix3D.position.clone();
		matrix3D.invert();
		matrix3D.transpose();
		const out: number[] = [];
		matrix3D.deltaTransformVectors([matrix.a, matrix.c, 0, matrix.b, matrix.d, 0], out);
		return new Matrix(out[0], out[3], out[1], out[4], matrix.tx - offset.x * out[0] - offset.y * out[1], matrix.ty - offset.x * out[3] - offset.y * out[4]);

	}

	public _setRenderState(renderRenderable: _Render_RenderableBase, shader: ShaderBase): void {
		super._setRenderState(renderRenderable, shader);

		const elements = <LineElements> this.elements;
		const view: View = shader.view;
		const renderElements = <_Render_LineElements> renderRenderable.entity
			.renderer.getRenderElements(renderRenderable.stageElements.elements);

		this._vao && this._vao.bind();

		const lineElements: LineElements = this.lineElements;

		if (!this._vao || this._vaoIsInvalid) {
			if (shader.colorBufferIndex >= 0)
				this.activateVertexBufferVO(shader.colorBufferIndex, lineElements.colors);

			this.activateVertexBufferVO(0, lineElements.positions, elements.dimension);
			this.activateVertexBufferVO(
				renderElements.secondaryPositionIndex,
				lineElements.positions,
				elements.dimension,
				elements.dimension * 2 * 2
			);
			this.activateVertexBufferVO(renderElements.thicknessIndex, lineElements.thickness);

			if (shader.uvIndex >= 0) {
				this.activateVertexBufferVO(shader.uvIndex, lineElements.positions, 2);
			}

			this._vaoIsInvalid = false;
		}

		const {
			oMisc
		} = renderElements.uOffsets;

		const data: Float32Array = shader.vertexConstantData;

		this._scale.copyFrom(renderRenderable.entity.node.getMatrix3D().decompose()[2]);

		const scaleMode: LineScaleMode = lineElements.scaleMode;
		const half_thickness: number = lineElements.half_thickness;
		if (scaleMode == LineScaleMode.NORMAL) {
			data[oMisc + 0] = (
				// eslint-disable-next-line max-len
				(half_thickness * this._scale.x / 1000 > 0.5 / (view.focalLength * view.pixelRatio))
					? this._scale.x / 1000
					: 0.5 / (half_thickness * view.focalLength * view.pixelRatio));
			data[oMisc + 1] = (half_thickness * this._scale.y / 1000 > 0.5 / view.focalLength)
				? this._scale.y / 1000
				: 0.5 / (half_thickness * view.focalLength);

		} else if (scaleMode == LineScaleMode.HAIRLINE) {
			data[oMisc + 0] = 1 / (view.focalLength * view.pixelRatio);
			data[oMisc + 1] = 1 / view.focalLength;
		} else {
			data[oMisc + 0] = 1 / Math.min(view.width, view.height);
			data[oMisc + 1] = 1 / Math.min(view.width, view.height);
		}
		data[oMisc + 2] = view.projection.near;
	}

	public draw(renderRenderable: _Render_RenderableBase, shader: ShaderBase, count: number, offset: number): void {
		const context: IContextGL = (<Stage> this._pool).context;

		// projection matrix
		shader.viewMatrix.copyFrom(shader.view.frustumMatrix3D, true);

		const matrix3D: Matrix3D = Matrix3D.CALCULATION_MATRIX;
		matrix3D.copyFrom(renderRenderable.entity.node.getMatrix3D());
		matrix3D.append(shader.view.projection.transform.inverseMatrix3D);
		shader.sceneMatrix.copyFrom(matrix3D, true);

		context.setProgramConstantsFromArray(ContextGLProgramType.VERTEX, shader.vertexConstantData);
		context.setProgramConstantsFromArray(ContextGLProgramType.FRAGMENT, shader.fragmentConstantData);

		if (this._indices)
			this.getIndexBufferGL().draw(ContextGLDrawMode.TRIANGLES, offset * 3, count * 3 || this.numIndices);
		else
			(<Stage> this._pool).context.drawVertices(ContextGLDrawMode.TRIANGLES, offset, count || this.numVertices);

		this._vao && this._vao.unbind();
	}

	/**
     * //TODO
     *
     * @param pool
     * @param renderRenderable
     * @param level
     * @param indexOffset
     * @returns {away.pool.LineSubSpriteRenderable}
     * @protected
     */
	public _pGetOverflowElements(): _Stage_ElementsBase {
		return <_Stage_ElementsBase> (<Stage> this._pool).abstractions.getNewAbstraction(this.lineElements);
	}
}

/**
 * @class away.pool._Render_LineElements
 */
export class _Render_LineElements extends _Render_ElementsBase {
	public secondaryPositionIndex: number = -1;

	public thicknessIndex: number = -1;

	public uOffsets = {
		oMisc: 0,
	};

	public _includeDependencies(shader: ShaderBase): void {
		//shader.colorDependencies++;
	}

	public _getVertexCode(
		shader: ShaderBase, registerCache: ShaderRegisterCache, sharedRegisters: ShaderRegisterData): string {
		//get the projection coordinates
		const position0: ShaderRegisterElement = (shader.globalPosDependencies > 0)
			? sharedRegisters.globalPositionVertex
			: sharedRegisters.animatedPosition;
		const position1: ShaderRegisterElement = registerCache.getFreeVertexAttribute();
		this.secondaryPositionIndex =  position1.index;

		const thickness: ShaderRegisterElement = registerCache.getFreeVertexAttribute();
		this.thicknessIndex =  thickness.index;

		//reserving vertex constants for projection matrix
		const viewMatrixReg: ShaderRegisterElement = registerCache.getFreeVertexConstant();
		registerCache.getFreeVertexConstant();
		registerCache.getFreeVertexConstant();
		registerCache.getFreeVertexConstant();
		shader.viewMatrixIndex = viewMatrixReg.index * 4;

		const misc = registerCache.getFreeVertexConstant();
		this.uOffsets.oMisc = misc.index * 4;

		const sceneMatrixReg = registerCache.getFreeVertexConstant();
		registerCache.getFreeVertexConstant();
		registerCache.getFreeVertexConstant();
		registerCache.getFreeVertexConstant();
		shader.sceneMatrixIndex = sceneMatrixReg.index * 4;

		const q0: ShaderRegisterElement = registerCache.getFreeVertexVectorTemp();
		registerCache.addVertexTempUsages(q0, 1);
		const q1: ShaderRegisterElement = registerCache.getFreeVertexVectorTemp();
		registerCache.addVertexTempUsages(q1, 1);

		const l: ShaderRegisterElement = registerCache.getFreeVertexVectorTemp();
		registerCache.addVertexTempUsages(l, 1);
		const behind: ShaderRegisterElement = registerCache.getFreeVertexVectorTemp();
		registerCache.addVertexTempUsages(behind, 1);
		const qclipped: ShaderRegisterElement = registerCache.getFreeVertexVectorTemp();
		registerCache.addVertexTempUsages(qclipped, 1);
		const offset: ShaderRegisterElement = registerCache.getFreeVertexVectorTemp();
		registerCache.addVertexTempUsages(offset, 1);

		// transform Q0 to eye space
		let code: string = 'm44 ' + q0 + ', ' + position0 + ', ' + sceneMatrixReg + '\n' +
			'm44 ' + q1 + ', ' + position1 + ', ' + sceneMatrixReg + '\n' + // transform Q1 to eye space
			'sub ' + l + ', ' + q1 + ', ' + q0 + '\n' + // L = Q1 - Q0

			// test if behind camera near plane
			// if 0 - Q0.z < Camera.near then the point needs to be clipped
			'slt ' + behind + '.x, ' + q0 + '.z, ' + misc + '.z\n' + // behind = ( 0 - Q0.z < -Camera.near ) ? 1 : 0
			'sub ' + behind + '.y, #native( 1.0 )native#, ' + behind + '.x\n' + // !behind = 1 - behind

		// p = point on the plane (0,0,-near)
		// n = plane normal (0,0,-1)
		// D = Q1 - Q0
		// t = ( dot( n, ( p - Q0 ) ) / ( dot( n, d )

			// solve for t where line crosses Camera.near
			'add ' + offset + '.x, ' + q0 + '.z, ' + misc + '.z\n' + // Q0.z + ( -Camera.near )
			'sub ' + offset + '.y, ' + q0 + '.z, ' + q1 + '.z\n' + // Q0.z - Q1.z

			// fix divide by zero for horizontal lines
			'seq ' + offset + '.z, ' + offset + '.y, #native( vec4(0.0) )native#\n' + // offset = (Q0.z - Q1.z)==0 ? 1 : 0
			'add ' + offset + '.y, ' + offset + '.y, ' + offset + '.z\n' + // ( Q0.z - Q1.z ) + offset

			'div ' + offset + '.z, ' + offset + '.x, ' + offset + '.y\n' + // t = ( Q0.z - near ) / ( Q0.z - Q1.z )

			'mul ' + offset + '.xyz, ' + offset + '.zzz, ' + l + '.xyz\n' + // t(L)
			'add ' + qclipped + '.xyz, ' + q0 + '.xyz, ' + offset + '.xyz\n' + // Qclipped = Q0 + t(L)
			'mov ' + qclipped + '.w, #native( 1.0 )native#\n' + // Qclipped.w = 1

			// If necessary, replace Q0 with new Qclipped
			'mul ' + q0 + ', ' + q0 + ', ' + behind + '.yyyy\n' + // !behind * Q0
			'mul ' + qclipped + ', ' + qclipped + ', ' + behind + '.xxxx\n' + // behind * Qclipped
			'add ' + q0 + ', ' + q0 + ', ' + qclipped + '\n' + // newQ0 = Q0 + Qclipped

			// calculate side vector for line
			'nrm ' + l + '.xyz, ' + l + '.xyz\n' + // normalize( L )
			'mov ' + behind + '.xyz, #native( vec4(0.0) )native#\n' + // D = normalize( Q1 )
			'mov ' + behind + '.z, #native( 1.0 )native#\n' + // D.w = 1
			'crs ' + qclipped + '.xyz, ' + l + ', ' + behind + '\n' + // S = L x D
			'nrm ' + qclipped + '.xyz, ' + qclipped + '.xyz\n' + // normalize( S )

			// face the side vector properly for the given point
			'mul ' + qclipped + '.xyz, ' + qclipped + '.xyz, ' + thickness + '.xxx\n' + // S *= weight
			'mov ' + qclipped + '.w, #native( 1.0 )native#\n' + // S.w = 1

			// calculate the amount required to move at the point's distance to correspond to the line's pixel width
			// scale the side vector by that amount
			'mul ' + offset + '.x, ' + q0 + '.z, #native( -1.0 )native#\n' + // distance = dot( view )
			'mul ' + qclipped + '.xyz, ' + qclipped + '.xyz, ' + offset + '.xxx\n' + // S.xyz *= pixelScaleFactor
			'mul ' + qclipped + '.xyz, ' + qclipped + '.xyz, ' + misc + '.xy\n' + // distance *= vpsod

			// add scaled side vector to Q0 and transform to clip space
			'add ' + q0 + '.xyz, ' + q0 + '.xyz, ' + qclipped + '.xyz\n' + // Q0 + S

			// transform Q0 to clip space
			'm44 op, ' + q0 + ', ' + viewMatrixReg + '\n';

		//Calculate the (possibly animated) UV coordinates.
		if (shader.uvDependencies > 0) {

			sharedRegisters.uvVarying = registerCache.getFreeVarying();

			if (shader.usesUVTransform) {
				// a, b, 0, tx
				// c, d, 0, ty
				const uvTransform1: ShaderRegisterElement = registerCache.getFreeVertexConstant();
				const uvTransform2: ShaderRegisterElement = registerCache.getFreeVertexConstant();
				shader.uvMatrixIndex = uvTransform1.index * 4;

				code += 'dp4 ' + sharedRegisters.uvVarying + '.x, ' +  q0 + ', ' + uvTransform1 + '\n' +
					'dp4 ' + sharedRegisters.uvVarying + '.y, ' +  q0 + ', ' + uvTransform2 + '\n' +
					'mov ' + sharedRegisters.uvVarying + '.zw, ' +  q0 + '.zw \n';
			} else {
				code += 'mov ' + sharedRegisters.uvVarying + ', ' + sharedRegisters.animatedUV + '\n';
			}
		}

		registerCache.removeVertexTempUsage(q0);
		registerCache.removeVertexTempUsage(q1);
		registerCache.removeVertexTempUsage(l);
		registerCache.removeVertexTempUsage(behind);
		registerCache.removeVertexTempUsage(qclipped);
		registerCache.removeVertexTempUsage(offset);

		return code;
	}

	public _getFragmentCode(
		shader: ShaderBase,
		registerCache: ShaderRegisterCache,
		sharedRegisters: ShaderRegisterData): string {
		return '';
	}
}

RenderGroup.registerElements(_Render_LineElements, LineElements);
Stage.registerAbstraction(_Stage_LineElements, LineElements);