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

namespace Marionette.EditorTools;

/// <summary>
/// Mirror: reflect the selected geometry across a line you click. Onshape's sketch Mirror.
///
/// THE ONE TOOL HERE THAT CONSUMES A SELECTION rather than making one. Every other tool acts on
/// whatever is under the cursor when it is pressed; this acts on what was picked with the Select
/// tool before it was armed, and its single click only names the axis. That is why it needs no
/// dialog and no phase of its own - the box select in EffigyViewport.Constraints.cs is the half
/// that collects, and this is the half that acts.
///
/// THE REFLECTION ITSELF IS THE KERNEL'S - see SketchEdit.Mirror, which also records the Symmetric
/// constraints that make it a mirror rather than a paste. Nothing here computes geometry except
/// the preview, and that calls the same SketchEdit.Reflect the commit will, so the ghost cannot
/// disagree with the result.
/// </summary>
internal sealed partial class EffigyViewport
{
	/// <summary>The curves currently selected, resolved. Curves that have since been deleted drop
	/// out rather than arriving as nulls.</summary>
	private List<SketchCurve> SelectedSketchCurves() =>
		SketchSelection.Curves.Select( FindCurve ).Where( c => c is not null ).ToList();

	/// <summary>The line under the cursor, or null when what is there is not a line. A mirror axis
	/// has to be straight, and the refusal has to name that rather than doing nothing.</summary>
	private SketchLine MirrorAxisUnderCursor() => FindCurve( CurveUnderCursor() ) as SketchLine;

	private void ApplyMirror( Vec2 p )
	{
		if ( !HasSketchSelection )
		{
			SetSketchPrompt( "Mirror - pick what to mirror with the Select tool first (drag a box "
				+ "round it), then click the line to reflect it across." );
			return;
		}

		if ( CurveUnderCursor() is not { } id || FindCurve( id ) is not { } curve )
		{
			SetSketchPrompt( "Mirror - click the line to reflect the selection across." );
			return;
		}

		if ( curve is not SketchLine axis )
		{
			SetSketchPrompt( "Mirror - the mirror line has to be a straight line; that is a "
				+ $"{curve.GetType().Name.Replace( "Sketch", "" ).ToLower()}." );
			return;
		}

		// NO SketchEditing HERE. ClickTool took the undo snapshot before handing the click on, the
		// same as Trim and Extend - a second snapshot makes the first Ctrl+Z look like it did
		// nothing.
		if ( !SketchEdit.Mirror( ActiveSketch, SelectedSketchCurves(), SketchSelection.Points,
			axis, out var created, out var error ) )
		{
			SetSketchPrompt( $"Mirror - {error}" );
			return;
		}

		SetSketchPrompt( created.Count == 1
			? "Mirror - one curve reflected across that line, and held symmetric to its original."
			: $"Mirror - {created.Count} curves reflected across that line, and held symmetric to "
				+ "their originals." );

		Edited();
	}

	/// <summary>The ghost of what the click would make, drawn across the line under the cursor.
	/// The axis is lit as well, because "which line am I about to mirror across" is the only
	/// question this tool asks.</summary>
	private void DrawMirrorPreview()
	{
		if ( !HasSketchSelection || MirrorAxisUnderCursor() is not { } axis )
			return;

		var a = ActiveSketch.Points[axis.Start];
		var b = ActiveSketch.Points[axis.End];

		Gizmo.Draw.Color = SketchSelectedColor;
		Gizmo.Draw.LineThickness = 3f;
		Gizmo.Draw.Line( PlaneToWorld( a ), PlaneToWorld( b ) );

		Gizmo.Draw.Color = SketchPreviewColor;
		Gizmo.Draw.LineThickness = 2f;

		foreach ( var curve in SelectedSketchCurves() )
		{
			if ( ReferenceEquals( curve, axis ) )
				continue;

			var points = curve.Tessellate( ActiveSketch, ActiveSketch.Tolerance );

			for ( var i = 0; i < points.Count - 1; i++ )
			{
				Gizmo.Draw.Line(
					PlaneToWorld( SketchEdit.Reflect( points[i], a, b ) ),
					PlaneToWorld( SketchEdit.Reflect( points[i + 1], a, b ) ) );
			}
		}

		var units = UnitsPerPixel();

		foreach ( var point in SketchSelection.Points )
		{
			Gizmo.Draw.SolidSphere(
				PlaneToWorld( SketchEdit.Reflect( ActiveSketch.Points[point], a, b ) ),
				units * PendingPointPixels, 8, 8 );
		}
	}

	private string MirrorPrompt()
	{
		if ( !HasSketchSelection )
			return "Mirror - pick what to mirror with the Select tool first (drag a box round it), "
				+ "then come back and click the line";

		var count = SketchSelection.Curves.Count + SketchSelection.Points.Count;

		return $"Mirror - click the line to reflect the {count} selected "
			+ $"thing{(count == 1 ? "" : "s")} across";
	}
}