Editor/EffigyEditor/EffigyViewport.MaterialBrush.cs
using Editor;
using Effigy;
using Sandbox;
using System;
using System.Collections.Generic;

namespace Marionette.EditorTools;

/// <summary>
/// Brushing an existing material onto faces.
///
/// THE SAME GESTURE AS THE COLOUR BRUSH AND A DIFFERENT RESULT. The ring, the hover, the
/// press-drag-release are all copied from <see cref="PaintFrame"/> deliberately: switching between
/// the two should feel like changing what the brush is loaded with, not like learning a tool. What
/// differs is what a dab does — colour blends into vertices here and now, a material dab is an
/// edit to the feature history, so it has to go out to the window, change the document and come
/// back as a rebuild.
///
/// WHICH IS WHY IT REPORTS RATHER THAN APPLIES. The viewport owns no studio and must not: it is
/// handed a mesh to look at. So a dab raises <see cref="MaterialDabbed"/> with the faces it
/// covered and the window does the assignment through MaterialDrop.Brush. That keeps the undo
/// story ordinary — a material dab is the same kind of edit as dropping a material, and undoes the
/// same way — instead of inventing a second undo path that only the brush knows about.
///
/// A DAB PER FRAME, AN UNDO STEP PER STROKE. The faces under the ring change as it moves, and each
/// batch has to reach the document while the drag is still happening or the model would not follow
/// the brush — so dabs are frequent. What a person expects to take back is the GESTURE, though, so
/// undo is recorded once, at the press, by <see cref="MaterialStrokeStarted"/>. Recording it per
/// dab instead would turn one sweep into fifty Ctrl+Z presses.
/// </summary>
internal sealed partial class EffigyViewport
{
	public MaterialBrushSession MaterialBrush { get; private set; }

	public bool IsMaterialBrushing => MaterialBrush is not null;

	/// <summary>Raised with the faces one dab covered. The window assigns them and rebuilds; the
	/// list is reused between dabs, so it must be consumed rather than kept.</summary>
	public Action<IReadOnlyList<int>> MaterialDabbed { get; set; }

	/// <summary>
	/// Raised once when a drag begins, BEFORE its first dab, so the window can record undo.
	///
	/// ONCE PER STROKE AND NOT PER DAB, which is the whole reason it is a separate signal. A dab
	/// fires every frame the brush moves, so recording undo there would make one sweep across a
	/// part into fifty Ctrl+Z presses; recording it nowhere - which is what this did at first -
	/// leaves the stroke unundoable and sends Ctrl+Z back past it to whatever came before. The
	/// gesture is the unit a person would expect to take back, so the gesture is what is recorded.
	/// </summary>
	public Action MaterialStrokeStarted { get; set; }

	/// <summary>
	/// Whether a material is actually chosen. The window sets it from the Materials browser.
	///
	/// THE RING HAS TO SAY SO. Without this the brush draws its ring, outlines the faces under it
	/// and then does nothing at all when dragged, because there is no material to lay down - which
	/// looks exactly like a broken tool rather than an unloaded one. Unloaded, the ring goes grey
	/// and stops outlining faces it is not going to take.
	/// </summary>
	public bool MaterialBrushLoaded { get; set; }

	private MeshHit? _materialCursor;
	private bool _materialStroking;

	public void BeginMaterialBrush( MaterialBrushSession session )
	{
		MaterialBrush = session ?? throw new ArgumentNullException( nameof( session ) );
		_materialStroking = false;
	}

	public void EndMaterialBrush()
	{
		MaterialBrush = null;
		_materialCursor = null;
		_materialStroking = false;
	}

	private void MaterialBrushFrame()
	{
		if ( MaterialBrush is null )
			return;

		_materialCursor = null;

		// Leaving the canvas does not end the drag, the same rule paint and sculpt keep: dragging
		// off the model and back on is ordinary.
		if ( _canvasHasCursor )
		{
			var ray = Gizmo.CurrentRay;
			var origin = new Vec3( ray.Position.x, ray.Position.y, ray.Position.z );
			var direction = new Vec3( ray.Forward.x, ray.Forward.y, ray.Forward.z );

			_materialCursor = MaterialBrush.Hover( origin, direction );

			// Before the first dab, so the snapshot is the state the stroke is about to change.
			if ( Gizmo.WasLeftMousePressed && !_materialStroking )
			{
				_materialStroking = true;
				MaterialStrokeStarted?.Invoke();
			}

			if ( _materialStroking && Gizmo.IsLeftMouseDown && _materialCursor is { } hit )
			{
				var faces = MaterialBrush.FacesAt( hit );

				if ( faces.Count > 0 )
					MaterialDabbed?.Invoke( faces );
			}
		}

		if ( _materialStroking && !Gizmo.IsLeftMouseDown )
			_materialStroking = false;

		DrawMaterialCursor();
	}

	/// <summary>
	/// The ring, plus the faces it is about to take.
	///
	/// HIGHLIGHTING THE FACES IS THE WHOLE POINT HERE, in a way it is not for colour. A material dab
	/// covers whole faces, so what the ring encloses and what the dab takes are not the same shape —
	/// on a coarse box a small ring still paints an entire side. Showing the faces means the tool
	/// says what it is about to do rather than letting the first click explain it.
	/// </summary>
	private void DrawMaterialCursor()
	{
		if ( _materialCursor is not { } hit )
			return;

		var mesh = MaterialBrush.Mesh;
		var colour = MaterialBrushLoaded ? MaterialCursorColor : UnloadedCursorColor;

		Gizmo.Draw.IgnoreDepth = true;
		Gizmo.Draw.Color = colour.WithAlpha( 0.5f );
		Gizmo.Draw.LineThickness = 1.5f;

		// Only when there is something to lay down: outlining faces the brush will not touch is a
		// promise it cannot keep.
		foreach ( var faceIndex in MaterialBrushLoaded ? MaterialBrush.FacesAt( hit ) : NoFaces )
		{
			if ( faceIndex < 0 || faceIndex >= mesh.Faces.Count )
				continue;

			var face = mesh.Faces[faceIndex];

			for ( var c = 0; c < face.Count; c++ )
			{
				var a = mesh.Positions[face.Indices[c]];
				var b = mesh.Positions[face.Indices[(c + 1) % face.Count]];

				Gizmo.Draw.Line( new Vector3( a.x, a.y, a.z ), new Vector3( b.x, b.y, b.z ) );
			}
		}

		var normal = new Vector3( hit.Normal.x, hit.Normal.y, hit.Normal.z ).Normal;
		var centre = new Vector3( hit.Point.x, hit.Point.y, hit.Point.z );

		var reference = MathF.Abs( normal.z ) > 0.9f ? new Vector3( 1f, 0f, 0f ) : new Vector3( 0f, 0f, 1f );
		var right = Vector3.Cross( normal, reference ).Normal;
		var up = Vector3.Cross( normal, right ).Normal;

		var radius = MaterialBrush.Radius;

		Gizmo.Draw.Color = colour;

		var lift = normal * (radius * 0.01f);
		const int Segments = 40;
		var previous = centre + right * radius + lift;

		for ( var i = 1; i <= Segments; i++ )
		{
			var angle = i / (float)Segments * MathF.PI * 2f;
			var point = centre + (right * MathF.Cos( angle ) + up * MathF.Sin( angle )) * radius + lift;

			Gizmo.Draw.Line( previous, point );
			previous = point;
		}
	}

	/// <summary>Amber, so it is neither sculpt's blue nor paint's pink — the three brushes are told
	/// apart by the colour of their ring before anything else.</summary>
	private static readonly Color MaterialCursorColor = new( 1f, 0.75f, 0.25f, 0.9f );

	/// <summary>Grey, for a brush with nothing loaded - the ring still tracks the surface so the
	/// tool is visibly alive, it just does not claim it is about to paint anything.</summary>
	private static readonly Color UnloadedCursorColor = new( 0.6f, 0.6f, 0.6f, 0.7f );

	private static readonly IReadOnlyList<int> NoFaces = new List<int>();
}