using System;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Mathematics;
using Unity.Jobs.LowLevel.Unsafe;
using UnityEngine;
using Unity.Burst;
using UnityEngine.Profiling;
using Unity.Collections;
using Unity.Jobs;

namespace Drawing {
	using static DrawingData;
	using static CommandBuilder;
	using Drawing.Text;
	using Unity.Profiling;
	using System.Collections.Generic;
	using UnityEngine.Rendering;

	static class GeometryBuilder {
		public struct CameraInfo {
			public float3 cameraPosition;
			public quaternion cameraRotation;
			public float2 cameraDepthToPixelSize;
			public bool cameraIsOrthographic;

			public CameraInfo(Camera camera) {
				var tr = camera?.transform;
				cameraPosition = tr != null ? (float3)tr.position : float3.zero;
				cameraRotation = tr != null ? (quaternion)tr.rotation : quaternion.identity;
				cameraDepthToPixelSize = (camera != null ? CameraDepthToPixelSize(camera) : 0);
				cameraIsOrthographic = camera != null ? camera.orthographic : false;
			}
		}

		internal static unsafe JobHandle Build (DrawingData gizmos, ProcessedBuilderData.MeshBuffers* buffers, ref CameraInfo cameraInfo, JobHandle dependency) {
			// Create a new builder and schedule it.
			// Why is characterInfo passed as a pointer and a length instead of just a NativeArray?
			// 	This is because passing it as a NativeArray invokes the safety system which adds some tracking to the NativeArray.
			//  This is normally not a problem, but we may be scheduling hundreds of jobs that use that particular NativeArray and this causes a bit of a slowdown
			//  in the safety checking system. Passing it as a pointer + length makes the whole scheduling code about twice as fast compared to passing it as a NativeArray.
			return new GeometryBuilderJob {
					   buffers = buffers,
					   currentMatrix = Matrix4x4.identity,
					   currentLineWidthData = new LineWidthData {
						   pixels = 1,
						   automaticJoins = false,
					   },
					   lineWidthMultiplier = DrawingManager.lineWidthMultiplier,
					   currentColor = (Color32)Color.white,
					   cameraPosition = cameraInfo.cameraPosition,
					   cameraRotation = cameraInfo.cameraRotation,
					   cameraDepthToPixelSize = cameraInfo.cameraDepthToPixelSize,
					   cameraIsOrthographic = cameraInfo.cameraIsOrthographic,
					   characterInfo = (SDFCharacter*)gizmos.fontData.characters.GetUnsafeReadOnlyPtr(),
					   characterInfoLength = gizmos.fontData.characters.Length,
					   maxPixelError = GeometryBuilderJob.MaxCirclePixelError / math.max(0.1f, gizmos.settingsRef.curveResolution),
			}.Schedule(dependency);
		}

		/// <summary>
		/// Helper for determining how large a pixel is at a given depth.
		/// A a distance D from the camera a pixel corresponds to roughly value.x * D + value.y world units.
		/// Where value is the return value from this function.
		/// </summary>
		private static float2 CameraDepthToPixelSize (Camera camera) {
			if (camera.orthographic) {
				return new float2(0.0f, 2.0f * camera.orthographicSize / camera.pixelHeight);
			} else {
				return new float2(Mathf.Tan(camera.fieldOfView * Mathf.Deg2Rad * 0.5f) / (0.5f * camera.pixelHeight), 0.0f);
			}
		}

		private static NativeArray<T> ConvertExistingDataToNativeArray<T>(UnsafeAppendBuffer data) where T : struct {
			unsafe {
				var arr = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray<T>(data.Ptr, data.Length / UnsafeUtility.SizeOf<T>(), Allocator.Invalid);
#if ENABLE_UNITY_COLLECTIONS_CHECKS
				NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref arr, AtomicSafetyHandle.GetTempMemoryHandle());
#endif
				return arr;
			}
		}

		internal static unsafe void BuildMesh (DrawingData gizmos, List<MeshWithType> meshes, ProcessedBuilderData.MeshBuffers* inputBuffers) {
			if (inputBuffers->triangles.Length > 0) {
				CommandBuilderSamplers.MarkerUpdateBuffer.Begin();
				var mesh = AssignMeshData<GeometryBuilderJob.Vertex>(gizmos, inputBuffers->bounds, inputBuffers->vertices, inputBuffers->triangles, MeshLayouts.MeshLayout);
				meshes.Add(new MeshWithType { mesh = mesh, type = MeshType.Lines });
				CommandBuilderSamplers.MarkerUpdateBuffer.End();
			}

			if (inputBuffers->solidTriangles.Length > 0) {
				var mesh = AssignMeshData<GeometryBuilderJob.Vertex>(gizmos, inputBuffers->bounds, inputBuffers->solidVertices, inputBuffers->solidTriangles, MeshLayouts.MeshLayout);
				meshes.Add(new MeshWithType { mesh = mesh, type = MeshType.Solid });
			}

			if (inputBuffers->textTriangles.Length > 0) {
				var mesh = AssignMeshData<GeometryBuilderJob.TextVertex>(gizmos, inputBuffers->bounds, inputBuffers->textVertices, inputBuffers->textTriangles, MeshLayouts.MeshLayoutText);
				meshes.Add(new MeshWithType { mesh = mesh, type = MeshType.Text });
			}
		}

		private static Mesh AssignMeshData<VertexType>(DrawingData gizmos, Bounds bounds, UnsafeAppendBuffer vertices, UnsafeAppendBuffer triangles, VertexAttributeDescriptor[] layout) where VertexType : struct {
			CommandBuilderSamplers.MarkerConvert.Begin();
			var verticesView = ConvertExistingDataToNativeArray<VertexType>(vertices);
			var trianglesView = ConvertExistingDataToNativeArray<int>(triangles);
			CommandBuilderSamplers.MarkerConvert.End();
			var mesh = gizmos.GetMesh(verticesView.Length);

			CommandBuilderSamplers.MarkerSetLayout.Begin();
			// Resize the vertex buffer if necessary
			// Note: also resized if the vertex buffer is significantly larger than necessary.
			//       This is because apparently when executing the command buffer Unity does something with the whole buffer for some reason (shows up as Mesh.CreateMesh in the profiler)
			// TODO: This could potentially cause bad behaviour if multiple meshes are used each frame and they have differing sizes.
			// We should query for meshes that already have an appropriately sized buffer.
			// if (mesh.vertexCount < verticesView.Length || mesh.vertexCount > verticesView.Length * 2) {

			// }
			// TODO: Use Mesh.GetVertexBuffer/Mesh.GetIndexBuffer once they stop being buggy.
			// Currently they don't seem to get refreshed properly after resizing them (2022.2.0b1)
			mesh.SetVertexBufferParams(math.ceilpow2(verticesView.Length), layout);
			mesh.SetIndexBufferParams(math.ceilpow2(trianglesView.Length), IndexFormat.UInt32);
			CommandBuilderSamplers.MarkerSetLayout.End();

			CommandBuilderSamplers.MarkerUpdateVertices.Begin();
			// Update the mesh data
			mesh.SetVertexBufferData(verticesView, 0, 0, verticesView.Length);
			CommandBuilderSamplers.MarkerUpdateVertices.End();
			CommandBuilderSamplers.MarkerUpdateIndices.Begin();
			// Update the index buffer and assume all our indices are correct
			mesh.SetIndexBufferData(trianglesView, 0, 0, trianglesView.Length, MeshUpdateFlags.DontValidateIndices);
			CommandBuilderSamplers.MarkerUpdateIndices.End();


			CommandBuilderSamplers.MarkerSubmesh.Begin();
			mesh.subMeshCount = 1;
			var submesh = new SubMeshDescriptor(0, trianglesView.Length, MeshTopology.Triangles) {
				vertexCount = verticesView.Length,
				bounds = bounds
			};
			mesh.SetSubMesh(0, submesh, MeshUpdateFlags.DontRecalculateBounds | MeshUpdateFlags.DontNotifyMeshUsers);
			mesh.bounds = bounds;
			CommandBuilderSamplers.MarkerSubmesh.End();
			return mesh;
		}
	}

	/// <summary>Some static fields that need to be in a separate class because Burst doesn't support them</summary>
	static class MeshLayouts {
		internal static readonly VertexAttributeDescriptor[] MeshLayout = {
			new VertexAttributeDescriptor(VertexAttribute.Position, VertexAttributeFormat.Float32, 3),
			new VertexAttributeDescriptor(VertexAttribute.Normal, VertexAttributeFormat.Float32, 3),
			new VertexAttributeDescriptor(VertexAttribute.Color, VertexAttributeFormat.UNorm8, 4),
			new VertexAttributeDescriptor(VertexAttribute.TexCoord0, VertexAttributeFormat.Float32, 2),
		};

		internal static readonly VertexAttributeDescriptor[] MeshLayoutText = {
			new VertexAttributeDescriptor(VertexAttribute.Position, VertexAttributeFormat.Float32, 3),
			new VertexAttributeDescriptor(VertexAttribute.Color, VertexAttributeFormat.UNorm8, 4),
			new VertexAttributeDescriptor(VertexAttribute.TexCoord0, VertexAttributeFormat.Float32, 2),
		};
	}

	/// <summary>
	/// Job to build the geometry from a stream of rendering commands.
	///
	/// See: <see cref="CommandBuilder"/>
	/// </summary>
	// Note: Setting FloatMode to Fast causes visual artificats when drawing circles.
	// I think it is because math.sin(float4) produces slightly different results
	// for each component in the input.
	[BurstCompile(FloatMode = FloatMode.Default)]
	internal struct GeometryBuilderJob : IJob {
		[NativeDisableUnsafePtrRestriction]
		public unsafe ProcessedBuilderData.MeshBuffers* buffers;

		[NativeDisableUnsafePtrRestriction]
		public unsafe SDFCharacter* characterInfo;
		public int characterInfoLength;

		public Color32 currentColor;
		public float4x4 currentMatrix;
		public LineWidthData currentLineWidthData;
		public float lineWidthMultiplier;
		float3 minBounds;
		float3 maxBounds;
		public float3 cameraPosition;
		public quaternion cameraRotation;
		public float2 cameraDepthToPixelSize;
		public float maxPixelError;
		public bool cameraIsOrthographic;

		[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
		public struct Vertex {
			public float3 position;
			public float3 uv2;
			public Color32 color;
			public float2 uv;
		}

		[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
		public struct TextVertex {
			public float3 position;
			public Color32 color;
			public float2 uv;
		}

		static unsafe void Add<T>(UnsafeAppendBuffer* buffer, T value) where T : unmanaged {
			int size = UnsafeUtility.SizeOf<T>();
			// We know that the buffer has enough capacity, so we can just write to the buffer without
			// having to add branches for the overflow case (like buffer->Add will do).
#if ENABLE_UNITY_COLLECTIONS_CHECKS
			UnityEngine.Assertions.Assert.IsTrue(buffer->Length + size <= buffer->Capacity);
#endif
			*(T*)(buffer->Ptr + buffer->Length) = value;
			buffer->Length = buffer->Length + size;
		}

		static unsafe void Reserve (UnsafeAppendBuffer* buffer, int size) {
			var newSize = buffer->Length + size;

			if (newSize > buffer->Capacity) {
				buffer->SetCapacity(math.max(newSize, buffer->Capacity * 2));
			}
		}

		internal static float3 PerspectiveDivide (float4 p) {
			return p.xyz * math.rcp(p.w);
		}

		unsafe void AddText (System.UInt16* text, TextData textData, Color32 color) {
			var pivot = PerspectiveDivide(math.mul(currentMatrix, new float4(textData.center, 1.0f)));

			AddTextInternal(
				text,
				pivot,
				math.mul(cameraRotation, new float3(1, 0, 0)),
				math.mul(cameraRotation, new float3(0, 1, 0)),
				textData.alignment,
				textData.sizeInPixels,
				true,
				textData.numCharacters,
				color
				);
		}

		unsafe void AddText3D (System.UInt16* text, TextData3D textData, Color32 color) {
			var pivot = PerspectiveDivide(math.mul(currentMatrix, new float4(textData.center, 1.0f)));
			var m = math.mul(currentMatrix, new float4x4(textData.rotation, float3.zero));

			AddTextInternal(
				text,
				pivot,
				m.c0.xyz,
				m.c1.xyz,
				textData.alignment,
				textData.size,
				false,
				textData.numCharacters,
				color
				);
		}


		unsafe void AddTextInternal (System.UInt16* text, float3 pivot, float3 right, float3 up, LabelAlignment alignment, float size, bool sizeIsInPixels, int numCharacters, Color32 color) {
			var distance = math.abs(math.dot(pivot - cameraPosition, math.mul(cameraRotation, new float3(0, 0, 1))));
			var pixelSize = cameraDepthToPixelSize.x * distance + cameraDepthToPixelSize.y;
			float fontWorldSize = size;

			if (sizeIsInPixels) fontWorldSize *= pixelSize;

			right *= fontWorldSize;
			up *= fontWorldSize;

			// Calculate the total width (in pixels divided by fontSize) of the text
			float maxWidth = 0;
			float currentWidth = 0;
			float numLines = 1;

			for (int i = 0; i < numCharacters; i++) {
				var characterInfoIndex = text[i];
				if (characterInfoIndex == SDFLookupData.Newline) {
					maxWidth = math.max(maxWidth, currentWidth);
					currentWidth = 0;
					numLines++;
				} else {
#if ENABLE_UNITY_COLLECTIONS_CHECKS
					if (characterInfoIndex >= characterInfoLength) throw new System.Exception("Invalid character. No info exists. This is a bug.");
#endif
					currentWidth += characterInfo[characterInfoIndex].advance;
				}
			}
			maxWidth = math.max(maxWidth, currentWidth);

			// Calculate the world space position of the text given the camera and text alignment
			var pos = pivot;
			pos -= right * maxWidth * alignment.relativePivot.x;
			// Size of a character as a fraction of a whole line using the current font
			const float FontCharacterFractionOfLine = 0.75f;
			// Where the upper and lower parts of the text will be assuming we start to write at y=0
			var lower = 1 - numLines;
			var upper = FontCharacterFractionOfLine;
			var yAdjustment = math.lerp(lower, upper, alignment.relativePivot.y);
			pos -= up * yAdjustment;
			pos += math.mul(cameraRotation, new float3(1, 0, 0)) * (pixelSize * alignment.pixelOffset.x);
			pos += math.mul(cameraRotation, new float3(0, 1, 0)) * (pixelSize * alignment.pixelOffset.y);

			var textVertices = &buffers->textVertices;
			var textTriangles = &buffers->textTriangles;

			// Reserve all buffer space beforehand
			Reserve(textVertices, numCharacters * VerticesPerCharacter * UnsafeUtility.SizeOf<TextVertex>());
			Reserve(textTriangles, numCharacters * TrianglesPerCharacter * UnsafeUtility.SizeOf<int>());

			var lineStart = pos;

			for (int i = 0; i < numCharacters; i++) {
				var characterInfoIndex = text[i];

				if (characterInfoIndex == SDFLookupData.Newline) {
					lineStart -= up;
					pos = lineStart;
					continue;
				}

				// Get character rendering information from the font
				SDFCharacter ch = characterInfo[characterInfoIndex];

				int vertexIndexStart = textVertices->Length / UnsafeUtility.SizeOf<TextVertex>();

				float3 v;

				v = pos + ch.vertexTopLeft.x * right + ch.vertexTopLeft.y * up;
				minBounds = math.min(minBounds, v);
				maxBounds = math.max(maxBounds, v);
				Add(textVertices, new TextVertex {
					position = v,
					uv = ch.uvTopLeft,
					color = color,
				});

				v = pos + ch.vertexTopRight.x * right + ch.vertexTopRight.y * up;
				minBounds = math.min(minBounds, v);
				maxBounds = math.max(maxBounds, v);
				Add(textVertices, new TextVertex {
					position = v,
					uv = ch.uvTopRight,
					color = color,
				});

				v = pos + ch.vertexBottomRight.x * right + ch.vertexBottomRight.y * up;
				minBounds = math.min(minBounds, v);
				maxBounds = math.max(maxBounds, v);
				Add(textVertices, new TextVertex {
					position = v,
					uv = ch.uvBottomRight,
					color = color,
				});

				v = pos + ch.vertexBottomLeft.x * right + ch.vertexBottomLeft.y * up;
				minBounds = math.min(minBounds, v);
				maxBounds = math.max(maxBounds, v);
				Add(textVertices, new TextVertex {
					position = v,
					uv = ch.uvBottomLeft,
					color = color,
				});

				Add(textTriangles, vertexIndexStart + 0);
				Add(textTriangles, vertexIndexStart + 1);
				Add(textTriangles, vertexIndexStart + 2);

				Add(textTriangles, vertexIndexStart + 0);
				Add(textTriangles, vertexIndexStart + 2);
				Add(textTriangles, vertexIndexStart + 3);

				// Advance character position
				pos += right * ch.advance;
			}
		}

		float3 lastNormalizedLineDir;
		float lastLineWidth;

		public const float MaxCirclePixelError = 0.5f;

		public const int VerticesPerCharacter = 4;
		public const int TrianglesPerCharacter = 6;

		void AddLine (LineData line) {
			// Store the line direction in the vertex.
			// A line consists of 4 vertices. The line direction will be used to
			// offset the vertices to create a line with a fixed pixel thickness
			var a = PerspectiveDivide(math.mul(currentMatrix, new float4(line.a, 1.0f)));
			var b = PerspectiveDivide(math.mul(currentMatrix, new float4(line.b, 1.0f)));

			float lineWidth = currentLineWidthData.pixels;
			var normalizedLineDir = math.normalizesafe(b - a);

			if (math.any(math.isnan(normalizedLineDir))) throw new Exception("Nan line coordinates");
			if (lineWidth <= 0) {
				return;
			}

			// Update the bounding box
			minBounds = math.min(minBounds, math.min(a, b));
			maxBounds = math.max(maxBounds, math.max(a, b));

			unsafe {
				var outlineVertices = &buffers->vertices;

				// Make sure there is enough allocated capacity for 4 more vertices
				Reserve(outlineVertices, 4 * UnsafeUtility.SizeOf<Vertex>());

				// Insert 4 vertices
				// Doing it with pointers is faster, and this is the hottest
				// code of the whole gizmo drawing process.
				var ptr = (Vertex*)((byte*)outlineVertices->Ptr + outlineVertices->Length);

				var startLineDir = normalizedLineDir * lineWidth;
				var endLineDir = normalizedLineDir * lineWidth;

				// If dot(last dir, this dir) >= 0 => use join
				if (lineWidth > 1 && currentLineWidthData.automaticJoins && outlineVertices->Length > 2*UnsafeUtility.SizeOf<Vertex>()) {
					// has previous vertex
					Vertex* lastVertex1 = (Vertex*)(ptr - 1);
					Vertex* lastVertex2 = (Vertex*)(ptr - 2);

					var cosAngle = math.dot(normalizedLineDir, lastNormalizedLineDir);
					if (math.all(lastVertex2->position == a) && lastLineWidth == lineWidth && cosAngle >= -0.6f) {
						// Safety: tangent cannot be 0 because cosAngle > -1
						var tangent = normalizedLineDir + lastNormalizedLineDir;
						// From the law of cosines we get that
						// tangent.magnitude = sqrt(2)*sqrt(1+cosAngle)

						// Create join!
						// Trigonometry gives us
						// joinRadius = lineWidth / (2*cos(alpha / 2))
						// Using half angle identity for cos we get
						// joinRadius = lineWidth / (sqrt(2)*sqrt(1 + cos(alpha))
						// Since the tangent already has mostly the same factors we can simplify the calculation
						// normalize(tangent) * joinRadius * 2
						// = tangent / (sqrt(2)*sqrt(1+cosAngle)) * joinRadius * 2
						// = tangent * lineWidth / (1 + cos(alpha)
						var joinLineDir = tangent * lineWidth / (1 + cosAngle);

						startLineDir = joinLineDir;
						lastVertex1->uv2 = startLineDir;
						lastVertex2->uv2 = startLineDir;
					}
				}

				outlineVertices->Length = outlineVertices->Length + 4 * UnsafeUtility.SizeOf<Vertex>();
				*ptr++ = new Vertex {
					position = a,
					color = currentColor,
					uv = new float2(0, 0),
					uv2 = startLineDir,
				};
				*ptr++ = new Vertex {
					position = a,
					color = currentColor,
					uv = new float2(1, 0),
					uv2 = startLineDir,
				};

				*ptr++ = new Vertex {
					position = b,
					color = currentColor,
					uv = new float2(0, 1),
					uv2 = endLineDir,
				};
				*ptr++ = new Vertex {
					position = b,
					color = currentColor,
					uv = new float2(1, 1),
					uv2 = endLineDir,
				};

				lastNormalizedLineDir = normalizedLineDir;
				lastLineWidth = lineWidth;
			}
		}

		/// <summary>Calculate number of steps to use for drawing a circle at the specified point and radius to get less than the specified pixel error.</summary>
		internal static int CircleSteps (float3 center, float radius, float maxPixelError, ref float4x4 currentMatrix, float2 cameraDepthToPixelSize, float3 cameraPosition) {
			var centerv4 = math.mul(currentMatrix, new float4(center, 1.0f));

			if (math.abs(centerv4.w) < 0.0000001f) return 3;
			var cc = PerspectiveDivide(centerv4);
			// Take the maximum scale factor among the 3 axes.
			// If the current matrix has a uniform scale then they are all the same.
			var maxScaleFactor = math.sqrt(math.max(math.max(math.lengthsq(currentMatrix.c0.xyz), math.lengthsq(currentMatrix.c1.xyz)), math.lengthsq(currentMatrix.c2.xyz))) / centerv4.w;
			var realWorldRadius = radius * maxScaleFactor;
			var distance = math.length(cc - cameraPosition);

			var pixelSize = cameraDepthToPixelSize.x * distance + cameraDepthToPixelSize.y;
			// realWorldRadius += pixelSize * this.currentLineWidthData.pixels * 0.5f;
			var cosAngle = 1 - (maxPixelError * pixelSize) / realWorldRadius;
			int steps = cosAngle < 0 ? 3 : (int)math.ceil(math.PI / (math.acos(cosAngle)));
			return steps;
		}

		void AddCircle (CircleData circle) {
			// If the circle has a zero normal then just ignore it
			if (math.all(circle.normal == 0)) return;

			circle.normal = math.normalize(circle.normal);
			// Canonicalize
			if (circle.normal.y < 0) circle.normal = -circle.normal;

			float3 tangent1;
			if (math.all(math.abs(circle.normal - new float3(0, 1, 0)) < 0.001f)) {
				// The normal was (almost) identical to (0, 1, 0)
				tangent1 = new float3(0, 0, 1);
			} else {
				// Common case
				tangent1 = math.normalizesafe(math.cross(circle.normal, new float3(0, 1, 0)));
			}

			var ex = tangent1;
			var ey = circle.normal;
			var ez = math.cross(ey, ex);
			var oldMatrix = currentMatrix;

			currentMatrix = math.mul(currentMatrix, new float4x4(
				new float4(ex, 0) * circle.radius,
				new float4(ey, 0) * circle.radius,
				new float4(ez, 0) * circle.radius,
				new float4(circle.center, 1)
				));

			AddCircle(new CircleXZData {
				center = new float3(0, 0, 0),
				radius = 1,
				startAngle = 0,
				endAngle = 2 * math.PI,
			});

			currentMatrix = oldMatrix;
		}

		void AddDisc (CircleData circle) {
			// If the circle has a zero normal then just ignore it
			if (math.all(circle.normal == 0)) return;

			var steps = CircleSteps(circle.center, circle.radius, maxPixelError, ref currentMatrix, cameraDepthToPixelSize, cameraPosition);

			circle.normal = math.normalize(circle.normal);
			float3 tangent1;
			if (math.all(math.abs(circle.normal - new float3(0, 1, 0)) < 0.001f)) {
				// The normal was (almost) identical to (0, 1, 0)
				tangent1 = new float3(0, 0, 1);
			} else {
				// Common case
				tangent1 = math.cross(circle.normal, new float3(0, 1, 0));
			}

			float invSteps = 1.0f / steps;

			unsafe {
				var solidVertices = &buffers->solidVertices;
				var solidTriangles = &buffers->solidTriangles;
				Reserve(solidVertices, steps * UnsafeUtility.SizeOf<Vertex>());
				Reserve(solidTriangles, 3*(steps-2) * UnsafeUtility.SizeOf<int>());

				var matrix = math.mul(currentMatrix, Matrix4x4.TRS(circle.center, Quaternion.LookRotation(circle.normal, tangent1), new Vector3(circle.radius, circle.radius, circle.radius)));

				var mn = minBounds;
				var mx = maxBounds;
				int vertexCount = solidVertices->Length / UnsafeUtility.SizeOf<Vertex>();

				for (int i = 0; i < steps; i++) {
					var t = math.lerp(0, 2*Mathf.PI, i * invSteps);
					math.sincos(t, out float sin, out float cos);

					var p = PerspectiveDivide(math.mul(matrix, new float4(cos, sin, 0, 1)));
					// Update the bounding box
					mn = math.min(mn, p);
					mx = math.max(mx, p);

					Add(solidVertices, new Vertex {
						position = p,
						color = currentColor,
						uv = new float2(0, 0),
						uv2 = new float3(0, 0, 0),
					});
				}

				minBounds = mn;
				maxBounds = mx;

				for (int i = 0; i < steps - 2; i++) {
					Add(solidTriangles, vertexCount);
					Add(solidTriangles, vertexCount + i + 1);
					Add(solidTriangles, vertexCount + i + 2);
				}
			}
		}

		void AddSphereOutline (SphereData circle) {
			var centerv4 = math.mul(currentMatrix, new float4(circle.center, 1.0f));

			if (math.abs(centerv4.w) < 0.0000001f) return;
			var center = PerspectiveDivide(centerv4);
			// Figure out the actual radius of the sphere after all the matrix multiplications.
			// In case of a non-uniform scale, pick the largest radius
			var maxScaleFactor = math.sqrt(math.max(math.max(math.lengthsq(currentMatrix.c0.xyz), math.lengthsq(currentMatrix.c1.xyz)), math.lengthsq(currentMatrix.c2.xyz))) / centerv4.w;
			var realWorldRadius = circle.radius * maxScaleFactor;

			if (cameraIsOrthographic) {
				var prevMatrix = this.currentMatrix;
				this.currentMatrix = float4x4.identity;
				AddCircle(new CircleData {
					center = center,
					normal = math.mul(this.cameraRotation, new float3(0, 0, 1)),
					radius = realWorldRadius,
				});
				this.currentMatrix = prevMatrix;
			} else {
				var dist = math.length(this.cameraPosition - center);
				// Camera is inside the sphere, cannot draw
				if (dist <= realWorldRadius) return;

				var offsetTowardsCamera = realWorldRadius * realWorldRadius / dist;
				var outlineRadius = math.sqrt(realWorldRadius * realWorldRadius - offsetTowardsCamera * offsetTowardsCamera);
				var normal = math.normalize(this.cameraPosition - center);
				var prevMatrix = this.currentMatrix;
				this.currentMatrix = float4x4.identity;
				AddCircle(new CircleData {
					center = center + normal * offsetTowardsCamera,
					normal = normal,
					radius = outlineRadius,
				});
				this.currentMatrix = prevMatrix;
			}
		}

		void AddCircle (CircleXZData circle) {
			circle.endAngle = math.clamp(circle.endAngle, circle.startAngle - Mathf.PI * 2, circle.startAngle + Mathf.PI * 2);

			unsafe {
				var m = math.mul(currentMatrix, new float4x4(
					new float4(circle.radius, 0, 0, 0),
					new float4(0, circle.radius, 0, 0),
					new float4(0, 0, circle.radius, 0),
					new float4(circle.center, 1)
					));
				var steps = CircleSteps(float3.zero, 1.0f, maxPixelError, ref m, cameraDepthToPixelSize, cameraPosition);
				var lineWidth = currentLineWidthData.pixels;
				if (lineWidth < 0) return;

				var byteSize = steps * 4 * UnsafeUtility.SizeOf<Vertex>();
				Reserve(&buffers->vertices, byteSize);
				var ptr = (Vertex*)(buffers->vertices.Ptr + buffers->vertices.Length);
				buffers->vertices.Length += byteSize;
				math.sincos(circle.startAngle, out float sin0, out float cos0);
				var prev = PerspectiveDivide(math.mul(m, new float4(cos0, 0, sin0, 1)));
				var prevTangent = math.normalizesafe(math.mul(m, new float4(-sin0, 0, cos0, 0)).xyz) * lineWidth;
				var invSteps = math.rcp(steps);

				for (int i = 1; i <= steps; i++) {
					var t = math.lerp(circle.startAngle, circle.endAngle, i * invSteps);
					math.sincos(t, out float sin, out float cos);
					var next = PerspectiveDivide(math.mul(m, new float4(cos, 0, sin, 1)));
					var tangent = math.normalizesafe(math.mul(m, new float4(-sin, 0, cos, 0)).xyz) * lineWidth;
					*ptr++ = new Vertex {
						position = prev,
						color = currentColor,
						uv = new float2(0, 0),
						uv2 = prevTangent,
					};
					*ptr++ = new Vertex {
						position = prev,
						color = currentColor,
						uv = new float2(1, 0),
						uv2 = prevTangent,
					};
					*ptr++ = new Vertex {
						position = next,
						color = currentColor,
						uv = new float2(0, 1),
						uv2 = tangent,
					};
					*ptr++ = new Vertex {
						position = next,
						color = currentColor,
						uv = new float2(1, 1),
						uv2 = tangent,
					};

					prev = next;
					prevTangent = tangent;
				}

				// Update the global bounds with the bounding box of the circle
				var b0 = PerspectiveDivide(math.mul(m, new float4(-1, 0, 0, 1)));
				var b1 = PerspectiveDivide(math.mul(m, new float4(0, -1, 0, 1)));
				var b2 = PerspectiveDivide(math.mul(m, new float4(+1, 0, 0, 1)));
				var b3 = PerspectiveDivide(math.mul(m, new float4(0, +1, 0, 1)));
				minBounds = math.min(math.min(math.min(math.min(b0, b1), b2), b3), minBounds);
				maxBounds = math.max(math.max(math.max(math.max(b0, b1), b2), b3), maxBounds);
			}
		}

		void AddDisc (CircleXZData circle) {
			var steps = CircleSteps(circle.center, circle.radius, maxPixelError, ref currentMatrix, cameraDepthToPixelSize, cameraPosition);

			circle.endAngle = math.clamp(circle.endAngle, circle.startAngle - Mathf.PI * 2, circle.startAngle + Mathf.PI * 2);

			float invSteps = 1.0f / steps;

			unsafe {
				var solidVertices = &buffers->solidVertices;
				var solidTriangles = &buffers->solidTriangles;
				Reserve(solidVertices, (2+steps) * UnsafeUtility.SizeOf<Vertex>());
				Reserve(solidTriangles, 3*steps * UnsafeUtility.SizeOf<int>());

				var matrix = math.mul(currentMatrix, Matrix4x4.Translate(circle.center) * Matrix4x4.Scale(new Vector3(circle.radius, circle.radius, circle.radius)));

				var worldCenter = PerspectiveDivide(math.mul(matrix, new float4(0, 0, 0, 1)));
				Add(solidVertices, new Vertex {
					position = worldCenter,
					color = currentColor,
					uv = new float2(0, 0),
					uv2 = new float3(0, 0, 0),
				});

				var mn = math.min(minBounds, worldCenter);
				var mx = math.max(maxBounds, worldCenter);
				int vertexCount = solidVertices->Length / UnsafeUtility.SizeOf<Vertex>();

				for (int i = 0; i <= steps; i++) {
					var t = math.lerp(circle.startAngle, circle.endAngle, i * invSteps);
					math.sincos(t, out float sin, out float cos);

					var p = PerspectiveDivide(math.mul(matrix, new float4(cos, 0, sin, 1)));
					// Update the bounding box
					mn = math.min(mn, p);
					mx = math.max(mx, p);

					Add(solidVertices, new Vertex {
						position = p,
						color = currentColor,
						uv = new float2(0, 0),
						uv2 = new float3(0, 0, 0),
					});
				}

				minBounds = mn;
				maxBounds = mx;

				for (int i = 0; i < steps; i++) {
					// Center vertex
					Add(solidTriangles, vertexCount - 1);
					Add(solidTriangles, vertexCount + i + 0);
					Add(solidTriangles, vertexCount + i + 1);
				}
			}
		}

		void AddSolidTriangle (TriangleData triangle) {
			unsafe {
				var solidVertices = &buffers->solidVertices;
				var solidTriangles = &buffers->solidTriangles;
				Reserve(solidVertices, 3 * UnsafeUtility.SizeOf<Vertex>());
				Reserve(solidTriangles, 3 * UnsafeUtility.SizeOf<int>());
				var matrix = currentMatrix;
				var a = PerspectiveDivide(math.mul(matrix, new float4(triangle.a, 1)));
				var b = PerspectiveDivide(math.mul(matrix, new float4(triangle.b, 1)));
				var c = PerspectiveDivide(math.mul(matrix, new float4(triangle.c, 1)));
				int startVertex = solidVertices->Length / UnsafeUtility.SizeOf<Vertex>();

				minBounds = math.min(math.min(math.min(minBounds, a), b), c);
				maxBounds = math.max(math.max(math.max(maxBounds, a), b), c);

				Add(solidVertices, new Vertex {
					position = a,
					color = currentColor,
					uv = new float2(0, 0),
					uv2 = new float3(0, 0, 0),
				});
				Add(solidVertices, new Vertex {
					position = b,
					color = currentColor,
					uv = new float2(0, 0),
					uv2 = new float3(0, 0, 0),
				});
				Add(solidVertices, new Vertex {
					position = c,
					color = currentColor,
					uv = new float2(0, 0),
					uv2 = new float3(0, 0, 0),
				});

				Add(solidTriangles, startVertex + 0);
				Add(solidTriangles, startVertex + 1);
				Add(solidTriangles, startVertex + 2);
			}
		}

		void AddWireBox (BoxData box) {
			var min = box.center - box.size * 0.5f;
			var max = box.center + box.size * 0.5f;
			AddLine(new LineData { a = new float3(min.x, min.y, min.z), b = new float3(max.x, min.y, min.z) });
			AddLine(new LineData { a = new float3(max.x, min.y, min.z), b = new float3(max.x, min.y, max.z) });
			AddLine(new LineData { a = new float3(max.x, min.y, max.z), b = new float3(min.x, min.y, max.z) });
			AddLine(new LineData { a = new float3(min.x, min.y, max.z), b = new float3(min.x, min.y, min.z) });

			AddLine(new LineData { a = new float3(min.x, max.y, min.z), b = new float3(max.x, max.y, min.z) });
			AddLine(new LineData { a = new float3(max.x, max.y, min.z), b = new float3(max.x, max.y, max.z) });
			AddLine(new LineData { a = new float3(max.x, max.y, max.z), b = new float3(min.x, max.y, max.z) });
			AddLine(new LineData { a = new float3(min.x, max.y, max.z), b = new float3(min.x, max.y, min.z) });

			AddLine(new LineData { a = new float3(min.x, min.y, min.z), b = new float3(min.x, max.y, min.z) });
			AddLine(new LineData { a = new float3(max.x, min.y, min.z), b = new float3(max.x, max.y, min.z) });
			AddLine(new LineData { a = new float3(max.x, min.y, max.z), b = new float3(max.x, max.y, max.z) });
			AddLine(new LineData { a = new float3(min.x, min.y, max.z), b = new float3(min.x, max.y, max.z) });
		}

		void AddPlane (PlaneData plane) {
			var oldMatrix = currentMatrix;

			currentMatrix = math.mul(currentMatrix, float4x4.TRS(plane.center, plane.rotation, new float3(plane.size.x * 0.5f, 1, plane.size.y * 0.5f)));

			AddLine(new LineData { a = new float3(-1, 0, -1), b = new float3(1, 0, -1) });
			AddLine(new LineData { a = new float3(1, 0, -1), b = new float3(1, 0, 1) });
			AddLine(new LineData { a = new float3(1, 0, 1), b = new float3(-1, 0, 1) });
			AddLine(new LineData { a = new float3(-1, 0, 1), b = new float3(-1, 0, -1) });

			currentMatrix = oldMatrix;
		}

		internal static readonly float4[] BoxVertices = {
			new float4(-1, -1, -1, 1),
			new float4(-1, -1, +1, 1),
			new float4(-1, +1, -1, 1),
			new float4(-1, +1, +1, 1),
			new float4(+1, -1, -1, 1),
			new float4(+1, -1, +1, 1),
			new float4(+1, +1, -1, 1),
			new float4(+1, +1, +1, 1),
		};

		internal static readonly int[] BoxTriangles = {
			// Bottom two triangles
			0, 1, 5,
			0, 5, 4,

			// Top
			7, 3, 2,
			7, 2, 6,

			// -X
			0, 1, 3,
			0, 3, 2,

			// +X
			4, 5, 7,
			4, 7, 6,

			// +Z
			1, 3, 7,
			1, 7, 5,

			// -Z
			0, 2, 6,
			0, 6, 4,
		};

		void AddBox (BoxData box) {
			unsafe {
				var solidVertices = &buffers->solidVertices;
				var solidTriangles = &buffers->solidTriangles;
				Reserve(solidVertices, BoxVertices.Length * UnsafeUtility.SizeOf<Vertex>());
				Reserve(solidTriangles, BoxTriangles.Length * UnsafeUtility.SizeOf<int>());

				var scale = box.size * 0.5f;
				var matrix = math.mul(currentMatrix, new float4x4(
					new float4(scale.x, 0, 0, 0),
					new float4(0, scale.y, 0, 0),
					new float4(0, 0, scale.z, 0),
					new float4(box.center, 1)
					));

				var mn = minBounds;
				var mx = maxBounds;
				int vertexOffset = solidVertices->Length / UnsafeUtility.SizeOf<Vertex>();
				var ptr = (Vertex*)(solidVertices->Ptr + solidVertices->Length);
				for (int i = 0; i < BoxVertices.Length; i++) {
					var p = PerspectiveDivide(math.mul(matrix, BoxVertices[i]));
					// Update the bounding box
					mn = math.min(mn, p);
					mx = math.max(mx, p);

					*ptr++ = new Vertex {
						position = p,
						color = currentColor,
						uv = new float2(0, 0),
						uv2 = new float3(0, 0, 0),
					};
				}
				solidVertices->Length += BoxVertices.Length * UnsafeUtility.SizeOf<Vertex>();

				minBounds = mn;
				maxBounds = mx;

				var triPtr = (int*)(solidTriangles->Ptr + solidTriangles->Length);
				for (int i = 0; i < BoxTriangles.Length; i++) {
					*triPtr++ = vertexOffset + BoxTriangles[i];
				}
				solidTriangles->Length += BoxTriangles.Length * UnsafeUtility.SizeOf<int>();
			}
		}

		// AggressiveInlining because this is only called from a single location, and burst doesn't inline otherwise
		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
		public void Next (ref UnsafeAppendBuffer.Reader reader, ref NativeArray<float4x4> matrixStack, ref NativeArray<Color32> colorStack, ref NativeArray<LineWidthData> lineWidthStack, ref int matrixStackSize, ref int colorStackSize, ref int lineWidthStackSize) {
			var fullCmd = reader.ReadNext<Command>();
			var cmd = fullCmd & (Command)0xFF;
			Color32 oldColor = default;

			if ((fullCmd & Command.PushColorInline) != 0) {
				oldColor = currentColor;
				currentColor = reader.ReadNext<Color32>();
			}

			switch (cmd) {
			case Command.PushColor:
#if ENABLE_UNITY_COLLECTIONS_CHECKS
				if (colorStackSize >= colorStack.Length) throw new System.Exception("Too deeply nested PushColor calls");
#else
				if (colorStackSize >= colorStack.Length) colorStackSize--;
#endif
				colorStack[colorStackSize] = currentColor;
				colorStackSize++;
				currentColor = reader.ReadNext<Color32>();
				break;
			case Command.PopColor:
#if ENABLE_UNITY_COLLECTIONS_CHECKS
				if (colorStackSize <= 0) throw new System.Exception("PushColor and PopColor are not matched");
#else
				if (colorStackSize <= 0) break;
#endif
				colorStackSize--;
				currentColor = colorStack[colorStackSize];
				break;
			case Command.PushMatrix:
#if ENABLE_UNITY_COLLECTIONS_CHECKS
				if (matrixStackSize >= matrixStack.Length) throw new System.Exception("Too deeply nested PushMatrix calls");
#else
				if (matrixStackSize >= matrixStack.Length) matrixStackSize--;
#endif
				matrixStack[matrixStackSize] = currentMatrix;
				matrixStackSize++;
				currentMatrix = math.mul(currentMatrix, reader.ReadNext<float4x4>());
				break;
			case Command.PushSetMatrix:
#if ENABLE_UNITY_COLLECTIONS_CHECKS
				if (matrixStackSize >= matrixStack.Length) throw new System.Exception("Too deeply nested PushMatrix calls");
#else
				if (matrixStackSize >= matrixStack.Length) matrixStackSize--;
#endif
				matrixStack[matrixStackSize] = currentMatrix;
				matrixStackSize++;
				currentMatrix = reader.ReadNext<float4x4>();
				break;
			case Command.PopMatrix:
#if ENABLE_UNITY_COLLECTIONS_CHECKS
				if (matrixStackSize <= 0) throw new System.Exception("PushMatrix and PopMatrix are not matched");
#else
				if (matrixStackSize <= 0) break;
#endif
				matrixStackSize--;
				currentMatrix = matrixStack[matrixStackSize];
				break;
			case Command.PushLineWidth:
#if ENABLE_UNITY_COLLECTIONS_CHECKS
				if (lineWidthStackSize >= lineWidthStack.Length) throw new System.Exception("Too deeply nested PushLineWidth calls");
#else
				if (lineWidthStackSize >= lineWidthStack.Length) lineWidthStackSize--;
#endif
				lineWidthStack[lineWidthStackSize] = currentLineWidthData;
				lineWidthStackSize++;
				currentLineWidthData = reader.ReadNext<LineWidthData>();
				currentLineWidthData.pixels *= lineWidthMultiplier;
				break;
			case Command.PopLineWidth:
#if ENABLE_UNITY_COLLECTIONS_CHECKS
				if (lineWidthStackSize <= 0) throw new System.Exception("PushLineWidth and PopLineWidth are not matched");
#else
				if (lineWidthStackSize <= 0) break;
#endif
				lineWidthStackSize--;
				currentLineWidthData = lineWidthStack[lineWidthStackSize];
				break;
			case Command.Line:
				AddLine(reader.ReadNext<LineData>());
				break;
			case Command.SphereOutline:
				AddSphereOutline(reader.ReadNext<SphereData>());
				break;
			case Command.CircleXZ:
				AddCircle(reader.ReadNext<CircleXZData>());
				break;
			case Command.Circle:
				AddCircle(reader.ReadNext<CircleData>());
				break;
			case Command.DiscXZ:
				AddDisc(reader.ReadNext<CircleXZData>());
				break;
			case Command.Disc:
				AddDisc(reader.ReadNext<CircleData>());
				break;
			case Command.Box:
				AddBox(reader.ReadNext<BoxData>());
				break;
			case Command.WirePlane:
				AddPlane(reader.ReadNext<PlaneData>());
				break;
			case Command.WireBox:
				AddWireBox(reader.ReadNext<BoxData>());
				break;
			case Command.SolidTriangle:
				AddSolidTriangle(reader.ReadNext<TriangleData>());
				break;
			case Command.PushPersist:
				// This command does not need to be handled by the builder
				reader.ReadNext<PersistData>();
				break;
			case Command.PopPersist:
				// This command does not need to be handled by the builder
				break;
			case Command.Text:
				var data = reader.ReadNext<TextData>();
				unsafe {
					System.UInt16* ptr = (System.UInt16*)reader.ReadNext(UnsafeUtility.SizeOf<System.UInt16>() * data.numCharacters);
					AddText(ptr, data, currentColor);
				}
				break;
			case Command.Text3D:
				var data2 = reader.ReadNext<TextData3D>();
				unsafe {
					System.UInt16* ptr = (System.UInt16*)reader.ReadNext(UnsafeUtility.SizeOf<System.UInt16>() * data2.numCharacters);
					AddText3D(ptr, data2, currentColor);
				}
				break;
			case Command.CaptureState:
				unsafe {
					buffers->capturedState.Add(new ProcessedBuilderData.CapturedState {
						color = this.currentColor,
						matrix = this.currentMatrix,
					});
				}
				break;
			default:
#if ENABLE_UNITY_COLLECTIONS_CHECKS
				throw new System.Exception("Unknown command");
#else
				break;
#endif
			}

			if ((fullCmd & Command.PushColorInline) != 0) {
				currentColor = oldColor;
			}
		}

		void CreateTriangles () {
			// Create triangles for all lines
			// A triangle consists of 3 indices
			// A line (4 vertices) consists of 2 triangles, so 6 triangle indices
			unsafe {
				var outlineVertices = &buffers->vertices;
				var outlineTriangles = &buffers->triangles;
				var vertexCount = outlineVertices->Length / UnsafeUtility.SizeOf<Vertex>();
				// Each line is made out of 4 vertices
				var lineCount = vertexCount / 4;
				var trianglesSizeInBytes = lineCount * 6 * UnsafeUtility.SizeOf<int>();
				if (trianglesSizeInBytes >= outlineTriangles->Capacity) {
					outlineTriangles->SetCapacity(math.ceilpow2(trianglesSizeInBytes));
				}

				int* ptr = (int*)outlineTriangles->Ptr;
				for (int i = 0, vi = 0; i < lineCount; i++, vi += 4) {
					// First triangle
					*ptr++ = vi + 0;
					*ptr++ = vi + 1;
					*ptr++ = vi + 2;

					// Second triangle
					*ptr++ = vi + 1;
					*ptr++ = vi + 3;
					*ptr++ = vi + 2;
				}
				outlineTriangles->Length = trianglesSizeInBytes;
			}
		}

		public const int MaxStackSize = 32;

		public void Execute () {
			unsafe {
				buffers->vertices.Reset();
				buffers->triangles.Reset();
				buffers->solidVertices.Reset();
				buffers->solidTriangles.Reset();
				buffers->textVertices.Reset();
				buffers->textTriangles.Reset();
				buffers->capturedState.Reset();
			}

			currentLineWidthData.pixels *= lineWidthMultiplier;

			minBounds = new float3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
			maxBounds = new float3(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity);

			var matrixStack = new NativeArray<float4x4>(MaxStackSize, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
			var colorStack = new NativeArray<Color32>(MaxStackSize, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
			var lineWidthStack = new NativeArray<LineWidthData>(MaxStackSize, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
			int matrixStackSize = 0;
			int colorStackSize = 0;
			int lineWidthStackSize = 0;

			CommandBuilderSamplers.MarkerProcessCommands.Begin();
			unsafe {
				var reader = buffers->splitterOutput.AsReader();
				while (reader.Offset < reader.Size) Next(ref reader, ref matrixStack, ref colorStack, ref lineWidthStack, ref matrixStackSize, ref colorStackSize, ref lineWidthStackSize);
#if ENABLE_UNITY_COLLECTIONS_CHECKS
				if (reader.Offset != reader.Size) throw new Exception("Didn't reach the end of the buffer");
#endif
			}
			CommandBuilderSamplers.MarkerProcessCommands.End();

			CommandBuilderSamplers.MarkerCreateTriangles.Begin();
			CreateTriangles();
			CommandBuilderSamplers.MarkerCreateTriangles.End();

			unsafe {
				var outBounds = &buffers->bounds;
				*outBounds = new Bounds((minBounds + maxBounds) * 0.5f, maxBounds - minBounds);

				if (math.any(math.isnan(outBounds->min)) && (buffers->vertices.Length > 0 || buffers->solidTriangles.Length > 0)) {
					// Fall back to a bounding box that covers everything
					*outBounds = new Bounds(Vector3.zero, new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity));
#if ENABLE_UNITY_COLLECTIONS_CHECKS
					throw new Exception("NaN bounds. A Draw.* command may have been given NaN coordinates.");
#endif
				}
			}
		}
	}
}
